From 3a9a59a3a84767dc75b8b9b362639c7cad19e407 Mon Sep 17 00:00:00 2001 From: Brian Reardon Date: Thu, 25 Sep 2025 17:58:05 -0700 Subject: [PATCH 01/13] changes --- taco/cmd/statesman/go.mod | 2 - taco/cmd/statesman/go.sum | 17 ++++-- taco/cmd/taco/go.mod | 21 +++++++- taco/cmd/taco/go.sum | 41 +++++++++++++- taco/providers/terraform/opentaco/go.mod | 34 +++++++++--- taco/providers/terraform/opentaco/go.sum | 69 +++++++++++++++++++----- 6 files changed, 158 insertions(+), 26 deletions(-) diff --git a/taco/cmd/statesman/go.mod b/taco/cmd/statesman/go.mod index ab1e60d3a..0d8ff53d0 100644 --- a/taco/cmd/statesman/go.mod +++ b/taco/cmd/statesman/go.mod @@ -27,11 +27,9 @@ require ( github.com/aws/aws-sdk-go-v2/service/sts v1.38.0 // indirect github.com/aws/smithy-go v1.22.5 // indirect github.com/coreos/go-oidc/v3 v3.11.0 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/go-jose/go-jose/v4 v4.0.2 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang-jwt/jwt/v5 v5.3.0 // indirect - github.com/google/go-cmp v0.7.0 // indirect github.com/google/jsonapi v1.0.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/labstack/gommon v0.4.2 // indirect diff --git a/taco/cmd/statesman/go.sum b/taco/cmd/statesman/go.sum index b76f29876..a029a0a70 100644 --- a/taco/cmd/statesman/go.sum +++ b/taco/cmd/statesman/go.sum @@ -37,6 +37,7 @@ github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI= github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= @@ -44,9 +45,11 @@ github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzq github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonapi v1.0.0 h1:qIGgO5Smu3yJmSs+QlvhQnrscdZfFhiV6S8ryJAglqU= github.com/google/jsonapi v1.0.0/go.mod h1:YYHiRPJT8ARXGER8In9VuLv4qvLfDmA9ULQqptbLE4s= 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/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8= github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= @@ -58,21 +61,27 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= -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/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +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/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= +golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 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.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/taco/cmd/taco/go.mod b/taco/cmd/taco/go.mod index bd1f23cd3..e8b9e067d 100644 --- a/taco/cmd/taco/go.mod +++ b/taco/cmd/taco/go.mod @@ -3,13 +3,32 @@ module github.com/diggerhq/digger/opentaco/cmd/taco go 1.24 require ( + github.com/diggerhq/digger/opentaco/internal v0.0.0-00010101000000-000000000000 github.com/diggerhq/digger/opentaco/pkg/sdk v0.0.0 - github.com/google/uuid v1.5.0 + github.com/google/uuid v1.6.0 github.com/mr-tron/base58 v1.2.0 github.com/spf13/cobra v1.8.0 ) require ( + github.com/aws/aws-sdk-go-v2 v1.38.1 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.0 // indirect + github.com/aws/aws-sdk-go-v2/config v1.31.2 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.18.6 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.4 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.4 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.4 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.4 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.87.1 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.28.2 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.2 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.38.0 // indirect + github.com/aws/smithy-go v1.22.5 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect ) diff --git a/taco/cmd/taco/go.sum b/taco/cmd/taco/go.sum index 2a4db1a6d..6c26e48c5 100644 --- a/taco/cmd/taco/go.sum +++ b/taco/cmd/taco/go.sum @@ -1,10 +1,49 @@ +github.com/aws/aws-sdk-go-v2 v1.38.1 h1:j7sc33amE74Rz0M/PoCpsZQ6OunLqys/m5antM0J+Z8= +github.com/aws/aws-sdk-go-v2 v1.38.1/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.0 h1:6GMWV6CNpA/6fbFHnoAjrv4+LGfyTqZz2LtCHnspgDg= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.0/go.mod h1:/mXlTIVG9jbxkqDnr5UQNQxW1HRYxeGklkM9vAFeabg= +github.com/aws/aws-sdk-go-v2/config v1.31.2 h1:NOaSZpVGEH2Np/c1toSeW0jooNl+9ALmsUTZ8YvkJR0= +github.com/aws/aws-sdk-go-v2/config v1.31.2/go.mod h1:17ft42Yb2lF6OigqSYiDAiUcX4RIkEMY6XxEMJsrAes= +github.com/aws/aws-sdk-go-v2/credentials v1.18.6 h1:AmmvNEYrru7sYNJnp3pf57lGbiarX4T9qU/6AZ9SucU= +github.com/aws/aws-sdk-go-v2/credentials v1.18.6/go.mod h1:/jdQkh1iVPa01xndfECInp1v1Wnp70v3K4MvtlLGVEc= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.4 h1:lpdMwTzmuDLkgW7086jE94HweHCqG+uOJwHf3LZs7T0= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.4/go.mod h1:9xzb8/SV62W6gHQGC/8rrvgNXU6ZoYM3sAIJCIrXJxY= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.4 h1:IdCLsiiIj5YJ3AFevsewURCPV+YWUlOW8JiPhoAy8vg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.4/go.mod h1:l4bdfCD7XyyZA9BolKBo1eLqgaJxl0/x91PL4Yqe0ao= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.4 h1:j7vjtr1YIssWQOMeOWRbh3z8g2oY/xPjnZH2gLY4sGw= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.4/go.mod h1:yDmJgqOiH4EA8Hndnv4KwAo8jCGTSnM5ASG1nBI+toA= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.4 h1:BE/MNQ86yzTINrfxPPFS86QCBNQeLKY2A0KhDh47+wI= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.4/go.mod h1:SPBBhkJxjcrzJBc+qY85e83MQ2q3qdra8fghhkkyrJg= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 h1:6+lZi2JeGKtCraAj1rpoZfKqnQ9SptseRZioejfUOLM= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0/go.mod h1:eb3gfbVIxIoGgJsi9pGne19dhCBpK6opTYpQqAmdy44= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.4 h1:Beh9oVgtQnBgR4sKKzkUBRQpf1GnL4wt0l4s8h2VCJ0= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.4/go.mod h1:b17At0o8inygF+c6FOD3rNyYZufPw62o9XJbSfQPgbo= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.4 h1:ueB2Te0NacDMnaC+68za9jLwkjzxGWm0KB5HTUHjLTI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.4/go.mod h1:nLEfLnVMmLvyIG58/6gsSA03F1voKGaCfHV7+lR8S7s= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.4 h1:HVSeukL40rHclNcUqVcBwE1YoZhOkoLeBfhUqR3tjIU= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.4/go.mod h1:DnbBOv4FlIXHj2/xmrUQYtawRFC9L9ZmQPz+DBc6X5I= +github.com/aws/aws-sdk-go-v2/service/s3 v1.87.1 h1:2n6Pd67eJwAb/5KCX62/8RTU0aFAAW7V5XIGSghiHrw= +github.com/aws/aws-sdk-go-v2/service/s3 v1.87.1/go.mod h1:w5PC+6GHLkvMJKasYGVloB3TduOtROEMqm15HSuIbw4= +github.com/aws/aws-sdk-go-v2/service/sso v1.28.2 h1:ve9dYBB8CfJGTFqcQ3ZLAAb/KXWgYlgu/2R2TZL2Ko0= +github.com/aws/aws-sdk-go-v2/service/sso v1.28.2/go.mod h1:n9bTZFZcBa9hGGqVz3i/a6+NG0zmZgtkB9qVVFDqPA8= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.2 h1:pd9G9HQaM6UZAZh19pYOkpKSQkyQQ9ftnl/LttQOcGI= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.2/go.mod h1:eknndR9rU8UpE/OmFpqU78V1EcXPKFTTm5l/buZYgvM= +github.com/aws/aws-sdk-go-v2/service/sts v1.38.0 h1:iV1Ko4Em/lkJIsoKyGfc0nQySi+v0Udxr6Igq+y9JZc= +github.com/aws/aws-sdk-go-v2/service/sts v1.38.0/go.mod h1:bEPcjW7IbolPfK67G1nilqWyoxYMSPrDiIQ3RdIdKgo= +github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw= +github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/taco/providers/terraform/opentaco/go.mod b/taco/providers/terraform/opentaco/go.mod index f346d8451..1918eea70 100644 --- a/taco/providers/terraform/opentaco/go.mod +++ b/taco/providers/terraform/opentaco/go.mod @@ -3,13 +3,34 @@ module github.com/diggerhq/digger/opentaco/providers/terraform/opentaco go 1.24 require ( + github.com/diggerhq/digger/opentaco/internal v0.0.0 github.com/diggerhq/digger/opentaco/pkg/sdk v0.0.0 github.com/hashicorp/terraform-plugin-framework v1.5.0 + github.com/mr-tron/base58 v1.2.0 ) require ( + github.com/aws/aws-sdk-go-v2 v1.38.1 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.0 // indirect + github.com/aws/aws-sdk-go-v2/config v1.31.2 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.18.6 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.4 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.4 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.4 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.4 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.87.1 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.28.2 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.2 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.38.0 // indirect + github.com/aws/smithy-go v1.22.5 // indirect github.com/fatih/color v1.13.0 // indirect github.com/golang/protobuf v1.5.3 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/go-hclog v1.5.0 // indirect github.com/hashicorp/go-plugin v1.6.0 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect @@ -18,19 +39,20 @@ require ( github.com/hashicorp/terraform-registry-address v0.2.3 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect github.com/hashicorp/yamux v0.1.1 // indirect - github.com/mattn/go-colorable v0.1.12 // indirect - github.com/mattn/go-isatty v0.0.14 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect - github.com/mr-tron/base58 v1.2.0 // indirect github.com/oklog/run v1.1.0 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - golang.org/x/net v0.19.0 // indirect - golang.org/x/sys v0.15.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/net v0.34.0 // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/text v0.21.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 // indirect google.golang.org/grpc v1.60.0 // indirect google.golang.org/protobuf v1.31.0 // indirect ) +replace github.com/diggerhq/digger/opentaco/internal => ../../../internal + replace github.com/diggerhq/digger/opentaco/pkg/sdk => ../../../pkg/sdk diff --git a/taco/providers/terraform/opentaco/go.sum b/taco/providers/terraform/opentaco/go.sum index 5931a7eec..193ab0b05 100644 --- a/taco/providers/terraform/opentaco/go.sum +++ b/taco/providers/terraform/opentaco/go.sum @@ -1,16 +1,55 @@ +github.com/aws/aws-sdk-go-v2 v1.38.1 h1:j7sc33amE74Rz0M/PoCpsZQ6OunLqys/m5antM0J+Z8= +github.com/aws/aws-sdk-go-v2 v1.38.1/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.0 h1:6GMWV6CNpA/6fbFHnoAjrv4+LGfyTqZz2LtCHnspgDg= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.0/go.mod h1:/mXlTIVG9jbxkqDnr5UQNQxW1HRYxeGklkM9vAFeabg= +github.com/aws/aws-sdk-go-v2/config v1.31.2 h1:NOaSZpVGEH2Np/c1toSeW0jooNl+9ALmsUTZ8YvkJR0= +github.com/aws/aws-sdk-go-v2/config v1.31.2/go.mod h1:17ft42Yb2lF6OigqSYiDAiUcX4RIkEMY6XxEMJsrAes= +github.com/aws/aws-sdk-go-v2/credentials v1.18.6 h1:AmmvNEYrru7sYNJnp3pf57lGbiarX4T9qU/6AZ9SucU= +github.com/aws/aws-sdk-go-v2/credentials v1.18.6/go.mod h1:/jdQkh1iVPa01xndfECInp1v1Wnp70v3K4MvtlLGVEc= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.4 h1:lpdMwTzmuDLkgW7086jE94HweHCqG+uOJwHf3LZs7T0= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.4/go.mod h1:9xzb8/SV62W6gHQGC/8rrvgNXU6ZoYM3sAIJCIrXJxY= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.4 h1:IdCLsiiIj5YJ3AFevsewURCPV+YWUlOW8JiPhoAy8vg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.4/go.mod h1:l4bdfCD7XyyZA9BolKBo1eLqgaJxl0/x91PL4Yqe0ao= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.4 h1:j7vjtr1YIssWQOMeOWRbh3z8g2oY/xPjnZH2gLY4sGw= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.4/go.mod h1:yDmJgqOiH4EA8Hndnv4KwAo8jCGTSnM5ASG1nBI+toA= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.4 h1:BE/MNQ86yzTINrfxPPFS86QCBNQeLKY2A0KhDh47+wI= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.4/go.mod h1:SPBBhkJxjcrzJBc+qY85e83MQ2q3qdra8fghhkkyrJg= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 h1:6+lZi2JeGKtCraAj1rpoZfKqnQ9SptseRZioejfUOLM= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0/go.mod h1:eb3gfbVIxIoGgJsi9pGne19dhCBpK6opTYpQqAmdy44= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.4 h1:Beh9oVgtQnBgR4sKKzkUBRQpf1GnL4wt0l4s8h2VCJ0= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.4/go.mod h1:b17At0o8inygF+c6FOD3rNyYZufPw62o9XJbSfQPgbo= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.4 h1:ueB2Te0NacDMnaC+68za9jLwkjzxGWm0KB5HTUHjLTI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.4/go.mod h1:nLEfLnVMmLvyIG58/6gsSA03F1voKGaCfHV7+lR8S7s= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.4 h1:HVSeukL40rHclNcUqVcBwE1YoZhOkoLeBfhUqR3tjIU= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.4/go.mod h1:DnbBOv4FlIXHj2/xmrUQYtawRFC9L9ZmQPz+DBc6X5I= +github.com/aws/aws-sdk-go-v2/service/s3 v1.87.1 h1:2n6Pd67eJwAb/5KCX62/8RTU0aFAAW7V5XIGSghiHrw= +github.com/aws/aws-sdk-go-v2/service/s3 v1.87.1/go.mod h1:w5PC+6GHLkvMJKasYGVloB3TduOtROEMqm15HSuIbw4= +github.com/aws/aws-sdk-go-v2/service/sso v1.28.2 h1:ve9dYBB8CfJGTFqcQ3ZLAAb/KXWgYlgu/2R2TZL2Ko0= +github.com/aws/aws-sdk-go-v2/service/sso v1.28.2/go.mod h1:n9bTZFZcBa9hGGqVz3i/a6+NG0zmZgtkB9qVVFDqPA8= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.2 h1:pd9G9HQaM6UZAZh19pYOkpKSQkyQQ9ftnl/LttQOcGI= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.2/go.mod h1:eknndR9rU8UpE/OmFpqU78V1EcXPKFTTm5l/buZYgvM= +github.com/aws/aws-sdk-go-v2/service/sts v1.38.0 h1:iV1Ko4Em/lkJIsoKyGfc0nQySi+v0Udxr6Igq+y9JZc= +github.com/aws/aws-sdk-go-v2/service/sts v1.38.0/go.mod h1:bEPcjW7IbolPfK67G1nilqWyoxYMSPrDiIQ3RdIdKgo= +github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw= +github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= 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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +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/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-plugin v1.6.0 h1:wgd4KxHJTVGGqWBq4QPB1i5BZNEx9BR8+OFmHDmTk8A= @@ -32,11 +71,14 @@ github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbg github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +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/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= @@ -46,23 +88,26 @@ github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DV 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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= 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-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +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.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 h1:6GQBEOdGkX6MMTLT9V+TjtIRZCw9VPD5Z+yHY9wMgS0= google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97/go.mod h1:v7nGkzlmW8P3n/bKmWBn2WpBjpOEx8Q6gMueudAmKfY= From 56ac2fefac1a2158ad9aee2fbc6bd8922c26a282 Mon Sep 17 00:00:00 2001 From: Brian Reardon Date: Mon, 29 Sep 2025 15:50:48 -0700 Subject: [PATCH 02/13] read and write working --- .gitignore | 5 +- go.mod | 9 + go.sum | 14 ++ taco/cmd/statesman/go.mod | 5 + taco/cmd/statesman/go.sum | 10 + taco/cmd/statesman/main.go | 15 +- taco/cmd/taco/commands/unit.go | 33 ++++ taco/internal/api/routes.go | 6 +- taco/internal/db/handler.go | 346 +++++++++++++++++++++++++++++++++ taco/internal/db/helpers.go | 246 +++++++++++++++++++++++ taco/internal/db/queries.go | 90 +++++++++ taco/internal/unit/handler.go | 93 ++++++++- taco/pkg/sdk/client.go | 34 ++++ 13 files changed, 900 insertions(+), 6 deletions(-) create mode 100644 go.sum create mode 100644 taco/internal/db/handler.go create mode 100644 taco/internal/db/helpers.go create mode 100644 taco/internal/db/queries.go diff --git a/.gitignore b/.gitignore index 47c3b72f3..dce203432 100644 --- a/.gitignore +++ b/.gitignore @@ -18,7 +18,7 @@ __azurite* go.work.sum # Taco specific binaries -taco + statesman terraform-provider-opentaco opentacosvc @@ -35,3 +35,6 @@ bin/ *.swp *.swo *~ + +#data +data/ \ No newline at end of file diff --git a/go.mod b/go.mod index 02d13b8c8..546539d76 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,12 @@ module github.com/diggerhq/digger go 1.24.0 + +require ( + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/mattn/go-sqlite3 v1.14.22 // indirect + golang.org/x/text v0.20.0 // indirect + gorm.io/driver/sqlite v1.6.0 // indirect + gorm.io/gorm v1.31.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 000000000..79b84992a --- /dev/null +++ b/go.sum @@ -0,0 +1,14 @@ +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.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +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= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= +gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= +gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= +gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= +gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY= +gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= diff --git a/taco/cmd/statesman/go.mod b/taco/cmd/statesman/go.mod index 0d8ff53d0..77144c459 100644 --- a/taco/cmd/statesman/go.mod +++ b/taco/cmd/statesman/go.mod @@ -32,9 +32,12 @@ require ( github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/google/jsonapi v1.0.0 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect github.com/labstack/gommon v0.4.2 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-sqlite3 v1.14.22 // indirect github.com/mr-tron/base58 v1.2.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/stretchr/testify v1.10.0 // indirect @@ -46,6 +49,8 @@ require ( golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.7.0 // indirect + gorm.io/driver/sqlite v1.6.0 // indirect + gorm.io/gorm v1.31.0 // indirect ) replace github.com/diggerhq/digger/opentaco/internal => ../../internal diff --git a/taco/cmd/statesman/go.sum b/taco/cmd/statesman/go.sum index a029a0a70..294c98935 100644 --- a/taco/cmd/statesman/go.sum +++ b/taco/cmd/statesman/go.sum @@ -50,6 +50,10 @@ github.com/google/jsonapi v1.0.0 h1:qIGgO5Smu3yJmSs+QlvhQnrscdZfFhiV6S8ryJAglqU= github.com/google/jsonapi v1.0.0/go.mod h1:YYHiRPJT8ARXGER8In9VuLv4qvLfDmA9ULQqptbLE4s= 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/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.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8= github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= @@ -59,6 +63,8 @@ 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/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/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= @@ -85,3 +91,7 @@ golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 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/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= +gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= +gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY= +gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= diff --git a/taco/cmd/statesman/main.go b/taco/cmd/statesman/main.go index 0122c1180..3e6a32b99 100644 --- a/taco/cmd/statesman/main.go +++ b/taco/cmd/statesman/main.go @@ -22,6 +22,7 @@ import ( "github.com/diggerhq/digger/opentaco/internal/analytics" "github.com/diggerhq/digger/opentaco/internal/api" "github.com/diggerhq/digger/opentaco/internal/storage" + "github.com/diggerhq/digger/opentaco/internal/db" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" ) @@ -37,6 +38,7 @@ func main() { ) flag.Parse() + database := db.OpenSQLite(db.DBConfig{}) // init db // Initialize storage var store storage.UnitStore switch *storageType { @@ -55,7 +57,16 @@ func main() { } else { store = s log.Printf("Using S3 storage: bucket=%s prefix=%s region=%s", *s3Bucket, *s3Prefix, *s3Region) - } + + + //put on thread thread / adjust seed so it accepts any store + // To this: + if s3Store, ok := store.(storage.S3Store); ok { + db.Seed(context.Background(), s3Store, database) + } else { + log.Println("Store is not S3Store, skipping seeding") + } + } default: store = storage.NewMemStore() log.Printf("Using in-memory storage") @@ -93,7 +104,7 @@ func main() { e.Use(middleware.Secure()) e.Use(middleware.CORS()) - api.RegisterRoutes(e, store, !*authDisable) + api.RegisterRoutes(e, store, !*authDisable, database) // Start server go func() { diff --git a/taco/cmd/taco/commands/unit.go b/taco/cmd/taco/commands/unit.go index b0995d200..cb604bfa5 100644 --- a/taco/cmd/taco/commands/unit.go +++ b/taco/cmd/taco/commands/unit.go @@ -44,6 +44,7 @@ func init() { unitCmd.AddCommand(unitVersionsCmd) unitCmd.AddCommand(unitRestoreCmd) unitCmd.AddCommand(unitStatusCmd) + unitCmd.AddCommand(unitLsFastCmd) } var unitCreateCmd = &cobra.Command{ @@ -185,6 +186,38 @@ var unitListCmd = &cobra.Command{ }, } +var unitLsFastCmd = &cobra.Command{ + Use: "ls-fast [prefix]", + Short: "List units using database (POC)", + Long: "List units using database lookups instead of S3 for RBAC resolution - proof of concept", + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + + + client := newAuthedClient() + + + prefix := "" + if len(args) > 0 { + prefix = args[0] + } + + result, err := client.ListUnitsFast(context.Background(), prefix) + if err != nil { + return fmt.Errorf("failed to list units: %w", err) + } + + // Display results with POC indicator + fmt.Printf("Units (via %s): %d\n", result.Source, result.Count) + for _, unit := range result.Units { + fmt.Printf(" %s\n", unit.ID) + } + + return nil + }, +} + + var unitInfoCmd = &cobra.Command{ Use: "info ", Short: "Show unit metadata information", diff --git a/taco/internal/api/routes.go b/taco/internal/api/routes.go index f4495295b..09d4cd7a1 100644 --- a/taco/internal/api/routes.go +++ b/taco/internal/api/routes.go @@ -21,10 +21,11 @@ import ( "github.com/diggerhq/digger/opentaco/internal/sts" "github.com/diggerhq/digger/opentaco/internal/storage" "github.com/labstack/echo/v4" + "gorm.io/gorm" ) // RegisterRoutes registers all API routes -func RegisterRoutes(e *echo.Echo, store storage.UnitStore, authEnabled bool) { +func RegisterRoutes(e *echo.Echo, store storage.UnitStore, authEnabled bool, db *gorm.DB) { // Health checks health := observability.NewHealthHandler() e.GET("/healthz", health.Healthz) @@ -146,13 +147,14 @@ func RegisterRoutes(e *echo.Echo, store storage.UnitStore, authEnabled bool) { } // Unit handlers (management API) - pass RBAC manager and signer for filtering - unitHandler := unithandlers.NewHandler(store, rbacManager, signer) + unitHandler := unithandlers.NewHandler(store, rbacManager, signer, db) // Management API (units) with RBAC middleware if authEnabled && rbacManager != nil { v1.POST("/units", middleware.RBACMiddleware(rbacManager, signer, rbac.ActionUnitWrite, "*")(unitHandler.CreateUnit)) // ListUnits does its own RBAC filtering internally, no middleware needed v1.GET("/units", unitHandler.ListUnits) + v1.GET("/units-fast", unitHandler.ListUnitsFast) v1.GET("/units/:id", middleware.RBACMiddleware(rbacManager, signer, rbac.ActionUnitRead, "{id}")(unitHandler.GetUnit)) v1.DELETE("/units/:id", middleware.RBACMiddleware(rbacManager, signer, rbac.ActionUnitDelete, "{id}")(unitHandler.DeleteUnit)) v1.GET("/units/:id/download", middleware.RBACMiddleware(rbacManager, signer, rbac.ActionUnitRead, "{id}")(unitHandler.DownloadUnit)) diff --git a/taco/internal/db/handler.go b/taco/internal/db/handler.go new file mode 100644 index 000000000..b2a26f8f9 --- /dev/null +++ b/taco/internal/db/handler.go @@ -0,0 +1,346 @@ +package db + +import ( + "log" + "time" + "gorm.io/gorm" + "gorm.io/driver/sqlite" + "gorm.io/gorm/logger" + "github.com/diggerhq/digger/opentaco/internal/storage" + rbac "github.com/diggerhq/digger/opentaco/internal/rbac" + "context" + "os" + "path/filepath" + +) + + +type Role struct { + ID int64 `gorm:"primaryKey"` + RoleId string `gorm:"not null;uniqueIndex"`// like "admin" + Name string //" admin role" + Description string // "Admin Role with full access" + Permissions []Permission `gorm:"many2many:role_permissions;constraint:OnDelete:CASCADE,OnUpdate:CASCADE"` + CreatedAt time.Time//timestamp + CreatedBy string //subject of creator (self for admin) +} + + + + +type Permission struct { + ID int64 `gorm:"primaryKey"` + PermissionId string `gorm:"not null;uniqueIndex"` + Name string // "admin permission" + Description string // "Admin permission allowing all action" + Rules []Rule `gorm:"constraint:OnDelete:CASCADE"` // [{"actions":["unit.read","unit.write","unit.lock","unit.delete","rbac.manage"],"resources":["*"],"effect":"allow"}] FK + CreatedBy string // subject of creator (self for admin) + CreatedAt time.Time +} + +type Rule struct { + ID int64 `gorm:"primaryKey"` + PermissionID int64 `gorm:"index;not null"` + Effect string `gorm:"size:8;not null;default:allow"` // "allow" | "deny" + WildcardAction bool `gorm:"not null;default:false"` + WildcardResource bool `gorm:"not null;default:false"` + Actions []RuleAction `gorm:"constraint:OnDelete:CASCADE"` + UnitTargets []RuleUnit `gorm:"constraint:OnDelete:CASCADE"` + TagTargets []RuleUnitTag `gorm:"constraint:OnDelete:CASCADE"` +} + + + +type RuleAction struct { + ID int64 `gorm:"primaryKey"` + RuleID int64 `gorm:"index;not null"` + Action string `gorm:"size:128;not null;index"` + // UNIQUE (rule_id, action) +} +func (RuleAction) TableName() string { return "rule_actions" } + +type RuleUnit struct { + ID int64 `gorm:"primaryKey"` + RuleID int64 `gorm:"index;not null"` + UnitID int64 `gorm:"index;not null"` + // UNIQUE (rule_id, resource_id) +} +func (RuleUnit) TableName() string { return "rule_units" } + +type RuleUnitTag struct { + ID int64 `gorm:"primaryKey"` + RuleID int64 `gorm:"index;not null"` + TagID int64 `gorm:"index;not null"` + // UNIQUE (rule_id, tag_id) +} +func (RuleUnitTag) TableName() string { return "rule_unit_tags" } + + + + +type User struct { + ID int64 `gorm:"primaryKey"` + Subject string `gorm:"not null;uniqueIndex"` + Email string `gorm:"not nulll;uniqueIndex"` + Roles []Role `gorm:"many2many:user_roles;constraint:OnDelete:CASCADE,OnUpdate:CASCADE"` + CreatedAt time.Time + UpdatedAt time.Time + Version int64 //"1" +} + +type Unit struct { + ID int64 `gorm:"primaryKey"` + Name string `gorm:"uniqueIndex"` + Tags []Tag `gorm:"many2many:unit_tags;constraint:OnDelete:CASCADE,OnUpdate:CASCADE"` + +} + +type Tag struct { + ID int64 `gorm:"primaryKey"` + Name string `gorm:"uniqueIndex"` + +} + + +//explicit joins + + +type UnitTag struct { + UnitID int64 `gorm:"primaryKey;index"` + TagID int64 `gorm:"primaryKey;index"` +} +func (UnitTag) TableName() string { return "unit_tags" } + + +type UserRole struct { + UserID int64 `gorm:"primaryKey;index"` + RoleID int64 `gorm:"primaryKey;index"` +} +func (UserRole) TableName() string { return "user_roles" } + + + +type RolePermission struct { + RoleID int64 `gorm:"primaryKey;index"` + PermissionID int64 `gorm:"primaryKey;index"` +} + + +func (RolePermission) TableName() string { return "role_permissions" } +/* + +todo + +ingest s3 +make adapter so this can be used +make UNIT LS look up with this sytem in the adapter as simple POC + + +*/ + + +var DefaultModels = []any{ + &User{}, + &Role{}, + &UserRole{}, + &Permission{}, + &Rule{}, + &RuleAction{}, + &RuleUnit{}, + &RuleUnitTag{}, + &RolePermission{}, + &Unit{}, + &Tag{}, + &UnitTag{}, +} + +type DBConfig struct { + Path string + Models []any +} + + +func OpenSQLite(cfg DBConfig) *gorm.DB { + + if cfg.Path == "" { + cfg.Path = "./data/taco.db" + + + if err := os.MkdirAll(filepath.Dir(cfg.Path), 0755); err != nil { + log.Fatalf("create db dir: %v", err) + } + + + + } + if len(cfg.Models) == 0 { cfg.Models = DefaultModels } + + // Keep DSN simple; set PRAGMAs via Exec (works reliably across drivers). + dsn := "file:" + cfg.Path + "?cache=shared" + + db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Info), // show SQL while developing + }) + if err != nil { + log.Fatalf("open sqlite: %v", err) + } + + // Connection pool hints (SQLite is single-writer; 1 open conn is safe) + sqlDB, err := db.DB() + if err != nil { + log.Fatalf("unwrap sql.DB: %v", err) + } + sqlDB.SetMaxOpenConns(1) + sqlDB.SetMaxIdleConns(1) + sqlDB.SetConnMaxLifetime(0) + + // Helpful PRAGMAs + if err := db.Exec(` + PRAGMA journal_mode=WAL; + PRAGMA foreign_keys=ON; + PRAGMA busy_timeout=5000; + `).Error; err != nil { + log.Fatalf("pragmas: %v", err) + } + + // AutoMigrate your models (add them below or pass via args) + if err := db.AutoMigrate(cfg.Models...); err != nil { + log.Fatalf("automigrate: %v", err) + } + + // Create the user-unit access view for fast ls-fast lookups + if err := db.Exec(` + CREATE VIEW IF NOT EXISTS user_unit_access AS + WITH user_permissions AS ( + SELECT DISTINCT + u.subject as user_subject, + r.id as rule_id, + r.wildcard_resource, + r.effect + FROM users u + JOIN user_roles ur ON u.id = ur.user_id + JOIN role_permissions rp ON ur.role_id = rp.role_id + JOIN rules r ON rp.permission_id = r.permission_id + LEFT JOIN rule_actions ra ON r.id = ra.rule_id + WHERE r.effect = 'allow' + AND (r.wildcard_action = 1 OR ra.action = 'unit.read' OR ra.action IS NULL) + ), + wildcard_access AS ( + SELECT DISTINCT + up.user_subject, + un.name as unit_name + FROM user_permissions up + CROSS JOIN units un + WHERE up.wildcard_resource = 1 + ), + specific_access AS ( + SELECT DISTINCT + up.user_subject, + un.name as unit_name + FROM user_permissions up + JOIN rule_units ru ON up.rule_id = ru.rule_id + JOIN units un ON ru.unit_id = un.id + WHERE up.wildcard_resource = 0 + ) + SELECT user_subject, unit_name FROM wildcard_access + UNION + SELECT user_subject, unit_name FROM specific_access; + `).Error; err != nil { + log.Printf("Warning: failed to create user_unit_access view: %v", err) + } + + + return db +} + + + + + + +// should make an adapter for this process, but for POC just s3store +func Seed(ctx context.Context, store storage.S3Store, db *gorm.DB){ + + + //gets called from service boot + + // call store + //for each document location + //get all the units TODO: consider tags + allUnits, err := store.List(ctx, "") + + if err != nil { + log.Fatal(err) + } + + //go through each unit + // should batch or use iter for scale - but proof of concept + // pagination via s3store would be trivial + for _, unit := range allUnits { + // create records + r := Unit{Name: unit.ID} + if err := db.FirstOrCreate(&r, Unit{Name: unit.ID}).Error; err != nil { + // if existed, r is loaded; else it’s created + log.Printf("Failed to create or find unit %s: %v", unit.ID, err) + continue + } + } + + // Right now there is no RBAC adapter either, outside of POC should actually implement this as well + S3RBACStore := rbac.NewS3RBACStore(store.GetS3Client(), store.GetS3Bucket(), store.GetS3Prefix()) + + + + //permission + permissions, err := S3RBACStore.ListPermissions(ctx) + if err != nil { + log.Fatal(err) + } + for _, permission := range permissions { + err := SeedPermission(ctx, db, permission) + if err != nil{ + log.Printf("Failed to seed permission: %s", permission.ID) + continue + } + } + + + //roles + roles, err := S3RBACStore.ListRoles(ctx) + if err != nil { + log.Fatal(err) + } + for _, role := range roles { + err := SeedRole(ctx, db, role) + if err != nil { + log.Printf("Failed to seed role: %s", role.ID) + continue + } + } + + + + //users + users, err := S3RBACStore.ListUserAssignments(ctx) + if err != nil { + log.Fatal(err) + } + for _, user := range users { + err := SeedUser(ctx,db,user) + if err != nil { + log.Printf("Failed to seed user: %s", user.Subject) + continue + } + + } + + + //TBD + //TFE tokens. + //system id section + //audit logs + //etc + + + +} diff --git a/taco/internal/db/helpers.go b/taco/internal/db/helpers.go new file mode 100644 index 000000000..cca7981cb --- /dev/null +++ b/taco/internal/db/helpers.go @@ -0,0 +1,246 @@ +package db + +import ( + "context" + "fmt" + "strings" + "gorm.io/gorm" + "gorm.io/gorm/clause" + "time" + + rbac "github.com/diggerhq/digger/opentaco/internal/rbac" +) + + +type S3RoleDoc struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Permissions []string `json:"permissions"` + CreatedAt time.Time `json:"created_at"` + CreatedBy string `json:"created_by"` + Version int64 `json:"version"` +} + + +type S3UserDoc struct { + Subject string `json:"subject"` + Email string `json:"email"` + Roles []string `json:"roles"` // e.g., ["admin","brian1-developer"] + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Version int64 `json:"version"` +} + + + +func hasStarResource(list []string) bool { + for _, s := range list { if s == "*" { return true } } + return false +} + +func hasStarAction(list []rbac.Action) bool { + for _, s := range list { if string(s) == "*" { return true } } + return false +} + +func SeedPermission(ctx context.Context, db *gorm.DB, s3Perm *rbac.Permission) error { + + + p := Permission{ + PermissionId: s3Perm.ID, + Name: s3Perm.Name, + Description: s3Perm.Description, + CreatedBy: s3Perm.CreatedBy, + CreatedAt: s3Perm.CreatedAt, + } + if err := db.WithContext(ctx).Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "permission_id"}}, + DoUpdates: clause.AssignmentColumns([]string{"name", "description", "created_by"}), + }).Create(&p).Error; err != nil { + return fmt.Errorf("permission upsert %s: %w", s3Perm.ID, err) + } + + // 2) Replace rules (simple + idempotent for seeds) + if err := db.WithContext(ctx). + Where("permission_id = ?", p.ID). + Delete(&Rule{}).Error; err != nil { + return fmt.Errorf("clear rules %s: %w", s3Perm.ID, err) + } + + for _, rr := range s3Perm.Rules { + rule := Rule{ + PermissionID: p.ID, + Effect: strings.ToLower(rr.Effect), + WildcardAction: hasStarAction(rr.Actions), + WildcardResource: hasStarResource(rr.Resources), + } + if err := db.WithContext(ctx).Create(&rule).Error; err != nil { + return fmt.Errorf("create rule: %w", err) + } + + // Only create children if not wildcard + if !rule.WildcardAction { + rows := make([]RuleAction, 0, len(rr.Actions)) + for _, a := range rr.Actions { + rows = append(rows, RuleAction{RuleID: rule.ID, Action: string(a)}) + } + if len(rows) > 0 { + if err := db.WithContext(ctx).Create(&rows).Error; err != nil { + return fmt.Errorf("actions: %w", err) + } + } + } + if !rule.WildcardResource { + // Resolve unit names -> Unit IDs, creating Units if missing + us := make([]RuleUnit, 0, len(rr.Resources)) + for _, name := range rr.Resources { + var u Unit + if err := db.WithContext(ctx). + Where(&Unit{Name: name}). + FirstOrCreate(&u).Error; err != nil { + return fmt.Errorf("ensure unit %q: %w", name, err) + } + us = append(us, RuleUnit{RuleID: rule.ID, UnitID: u.ID}) + } + if len(us) > 0 { + if err := db.WithContext(ctx).Create(&us).Error; err != nil { + return fmt.Errorf("units: %w", err) + } + } + } + } + return nil +} + + + + + + +func SeedRole(ctx context.Context, db *gorm.DB, rbacRole *rbac.Role) error { + return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + // 1) Upsert role by RoleId + var role Role + if err := tx. + Where(&Role{RoleId: rbacRole.ID}). + Attrs(Role{ + Name: rbacRole.Name, + Description: rbacRole.Description, + CreatedBy: rbacRole.CreatedBy, + CreatedAt: rbacRole.CreatedAt, // keep if you want to trust S3 timestamp + }). + FirstOrCreate(&role).Error; err != nil { + return fmt.Errorf("upsert role %q: %w", rbacRole.ID, err) + } + + // 2) Ensure all permissions exist (by PermissionId) + perms := make([]Permission, 0, len(rbacRole.Permissions)) + if len(rbacRole.Permissions) > 0 { + // fetch existing + var existing []Permission + if err := tx. + Where("permission_id IN ?", rbacRole.Permissions). + Find(&existing).Error; err != nil { + return fmt.Errorf("lookup permissions for role %q: %w", rbacRole.ID, err) + } + + exists := map[string]Permission{} + for _, p := range existing { + exists[p.PermissionId] = p + } + + // create any missing (minimal rows; names can be filled by permission seeder later) + for _, pid := range rbacRole.Permissions { + if p, ok := exists[pid]; ok { + perms = append(perms, p) + continue + } + np := Permission{ + PermissionId: pid, + Name: pid, // placeholder; your permission seeder will update + Description: "", + CreatedBy: rbacRole.CreatedBy, + } + if err := tx. + Where(&Permission{PermissionId: pid}). + Attrs(np). + FirstOrCreate(&np).Error; err != nil { + return fmt.Errorf("create missing permission %q: %w", pid, err) + } + perms = append(perms, np) + } + } + + // 3) Replace role -> permissions to match S3 exactly + // (idempotent; deletes any stale links, inserts new ones) + if err := tx.Model(&role).Association("Permissions").Replace(perms); err != nil { + return fmt.Errorf("set role permissions for %q: %w", rbacRole.ID, err) + } + + return nil + }) +} + + +func SeedUser(ctx context.Context, db *gorm.DB, rbacUser *rbac.UserAssignment) error { + return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + // 1) Upsert user by unique Subject + u := User{ + Subject: rbacUser.Subject, + Email: rbacUser.Email, + CreatedAt: rbacUser.CreatedAt, // optional: trust S3 timestamps + UpdatedAt: rbacUser.UpdatedAt, + Version: rbacUser.Version, + } + + // If row exists (subject unique), update mutable fields + if err := tx.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "subject"}}, + DoUpdates: clause.AssignmentColumns([]string{"email", "updated_at", "version"}), + }).Create(&u).Error; err != nil { + return fmt.Errorf("upsert user %q: %w", rbacUser.Subject, err) + } + + // Ensure we have the actual row (ID may be needed for associations) + if err := tx.Where(&User{Subject: rbacUser.Subject}).First(&u).Error; err != nil { + return fmt.Errorf("load user %q: %w", rbacUser.Subject, err) + } + + // 2) Ensure all roles exist (by RoleId); create placeholders if missing + roles := make([]Role, 0, len(rbacUser.Roles)) + if len(rbacUser.Roles) > 0 { + var existing []Role + if err := tx.Where("role_id IN ?", rbacUser.Roles).Find(&existing).Error; err != nil { + return fmt.Errorf("lookup roles: %w", err) + } + byID := make(map[string]Role, len(existing)) + for _, r := range existing { + byID[r.RoleId] = r + } + for _, rid := range rbacUser.Roles { + if r, ok := byID[rid]; ok { + roles = append(roles, r) + continue + } + nr := Role{ + RoleId: rid, + Name: rid, // placeholder; your role seeder can update later + Description: "", + CreatedBy: rbacUser.Subject, + } + if err := tx.Where(&Role{RoleId: rid}).Attrs(nr).FirstOrCreate(&nr).Error; err != nil { + return fmt.Errorf("create missing role %q: %w", rid, err) + } + roles = append(roles, nr) + } + } + + // 3) Set user->roles to exactly match the S3 doc + if err := tx.Model(&u).Association("Roles").Replace(roles); err != nil { + return fmt.Errorf("set user roles for %q: %w", rbacUser.Subject, err) + } + + return nil + }) +} \ No newline at end of file diff --git a/taco/internal/db/queries.go b/taco/internal/db/queries.go new file mode 100644 index 000000000..05374cadf --- /dev/null +++ b/taco/internal/db/queries.go @@ -0,0 +1,90 @@ +package db + +import ( + "gorm.io/gorm" + "log" +) + +func ListUnitsForUser(db *gorm.DB, userSubject string) ([]Unit, error) { + var units []Unit + + err := db.Where("id IN (?)", + db.Table("rule_units ru"). + Select("ru.unit_id"). + Joins("JOIN rules r ON ru.rule_id = r.id"). + Joins("JOIN role_permissions rp ON r.permission_id = rp.permission_id"). + Joins("JOIN user_roles ur ON rp.role_id = ur.role_id"). + Joins("JOIN users u ON ur.user_id = u.id"). + Where("u.subject = ? AND r.effect = 'allow'", userSubject)). + Preload("Tags"). + Find(&units).Error + + return units, err +} + + +// POC +// Replace S3Store.List +func ListAllUnits(db *gorm.DB, prefix string) ([]Unit, error) { + log.Println("ListAllUnits", prefix) + var units []Unit + query := db.Preload("Tags") + + if prefix != "" { + query = query.Where("name LIKE ?", prefix+"%") + } + + return units, query.Find(&units).Error +} + + + +// POC +func FilterUnitIDsByUser(db *gorm.DB, userSubject string, unitIDs []string) ([]string, error) { + log.Printf("FilterUnitIDsByUser: user=%s, checking %d units", userSubject, len(unitIDs)) + + if len(unitIDs) == 0 { + return []string{}, nil + } + + var allowedUnitIDs []string + + // Super simple query using the flattened view! + err := db.Table("user_unit_access"). + Select("unit_name"). + Where("user_subject = ?", userSubject). + Where("unit_name IN ?", unitIDs). + Pluck("unit_name", &allowedUnitIDs).Error + + log.Printf("User %s has access to %d/%d units", userSubject, len(allowedUnitIDs), len(unitIDs)) + return allowedUnitIDs, err +} + +func ListAllUnitsWithPrefix(db *gorm.DB, prefix string) ([]Unit, error) { + var units []Unit + query := db.Preload("Tags") + + if prefix != "" { + query = query.Where("name LIKE ?", prefix+"%") + } + + return units, query.Find(&units).Error +} + + + +/// POC - write to db example +// Sync functions to keep database in sync with storage operations +func SyncCreateUnit(db *gorm.DB, unitName string) error { + unit := Unit{Name: unitName} + return db.FirstOrCreate(&unit, Unit{Name: unitName}).Error +} + +func SyncDeleteUnit(db *gorm.DB, unitName string) error { + return db.Where("name = ?", unitName).Delete(&Unit{}).Error +} + +func SyncUnitExists(db *gorm.DB, unitName string) error { + unit := Unit{Name: unitName} + return db.FirstOrCreate(&unit, Unit{Name: unitName}).Error +} \ No newline at end of file diff --git a/taco/internal/unit/handler.go b/taco/internal/unit/handler.go index 4edbea4ee..33331240c 100644 --- a/taco/internal/unit/handler.go +++ b/taco/internal/unit/handler.go @@ -12,8 +12,12 @@ import ( "github.com/diggerhq/digger/opentaco/internal/deps" "github.com/diggerhq/digger/opentaco/internal/rbac" "github.com/diggerhq/digger/opentaco/internal/storage" + "github.com/diggerhq/digger/opentaco/internal/db" + "gorm.io/gorm" "github.com/google/uuid" "github.com/labstack/echo/v4" + "log" + ) // Handler serves the management API (unit CRUD and locking) @@ -21,13 +25,15 @@ type Handler struct { store storage.UnitStore rbacManager *rbac.RBACManager signer *auth.Signer + db *gorm.DB } -func NewHandler(store storage.UnitStore, rbacManager *rbac.RBACManager, signer *auth.Signer) *Handler { +func NewHandler(store storage.UnitStore, rbacManager *rbac.RBACManager, signer *auth.Signer, db *gorm.DB) *Handler { return &Handler{ store: store, rbacManager: rbacManager, signer: signer, + db: db, } } @@ -63,6 +69,14 @@ func (h *Handler) CreateUnit(c echo.Context) error { return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to create unit"}) } + // POC - write to db example + if h.db != nil { + if err := db.SyncCreateUnit(h.db, id); err != nil { + log.Printf("Warning: failed to sync unit creation to database: %v", err) + // Don't fail the request if DB sync fails + } + } + analytics.SendEssential("unit_created") return c.JSON(http.StatusCreated, CreateUnitResponse{ID: metadata.ID, Created: metadata.Updated}) } @@ -142,6 +156,16 @@ func (h *Handler) DeleteUnit(c echo.Context) error { } return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to delete unit"}) } + + // POC - write to db example + if h.db != nil { + if err := db.SyncDeleteUnit(h.db, id); err != nil { + log.Printf("Warning: failed to sync unit deletion to database: %v", err) + // Don't fail the request if DB sync fails + } + } + + return c.NoContent(http.StatusNoContent) } @@ -191,6 +215,12 @@ func (h *Handler) UploadUnit(c echo.Context) error { } return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to upload unit"}) } + // POC - write to db + if h.db != nil { + if err := db.SyncUnitExists(h.db, id); err != nil { + log.Printf("Warning: failed to sync unit to database: %v", err) + } + } // Best-effort dependency graph update go deps.UpdateGraphOnWrite(c.Request().Context(), h.store, id, data) analytics.SendEssential("taco_unit_push_completed") @@ -344,6 +374,67 @@ func (h *Handler) GetUnitStatus(c echo.Context) error { return c.JSON(http.StatusOK, st) } + +// POC +// Add this new method to the existing Handler struct +func (h *Handler) ListUnitsFast(c echo.Context) error { + prefix := c.QueryParam("prefix") + + // 1. Get all units from DATABASE + allUnits, err := db.ListAllUnitsWithPrefix(h.db, prefix) + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to list units from database"}) + } + + // 2. Extract unit names and create map + unitNames := make([]string, 0, len(allUnits)) + unitMap := make(map[string]db.Unit) + + for _, unit := range allUnits { + unitNames = append(unitNames, unit.Name) + unitMap[unit.Name] = unit + } + + // 3. RBAC filter with DATABASE + if h.rbacManager != nil && h.signer != nil { + principal, err := h.getPrincipalFromToken(c) + if err != nil { + if enabled, _ := h.rbacManager.IsEnabled(c.Request().Context()); enabled { + return c.JSON(http.StatusUnauthorized, map[string]string{"error": "Failed to authenticate user"}) + } + } else { + // RBAC filtering + filteredNames, err := db.FilterUnitIDsByUser(h.db, principal.Subject, unitNames) + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to check permissions via database"}) + } + unitNames = filteredNames + } + } + + // 4. Build response + var responseUnits []*domain.Unit + for _, name := range unitNames { + if dbUnit, exists := unitMap[name]; exists { + // Convert db.Unit to domain.Unit + responseUnits = append(responseUnits, &domain.Unit{ + ID: dbUnit.Name, + Size: 0, // DB doesn't have size, could be calculated + Updated: time.Now(), // Could add timestamp to db.Unit + Locked: false, // Could check locks in database + }) + } + } + + domain.SortUnitsByID(responseUnits) + return c.JSON(http.StatusOK, map[string]interface{}{ + "units": responseUnits, + "count": len(responseUnits), + "source": "database", // POC identifier + }) +} + + // Helpers func convertLockInfo(info *storage.LockInfo) *domain.Lock { if info == nil { return nil } diff --git a/taco/pkg/sdk/client.go b/taco/pkg/sdk/client.go index 1e5936b2a..12a8bf679 100644 --- a/taco/pkg/sdk/client.go +++ b/taco/pkg/sdk/client.go @@ -418,3 +418,37 @@ func (c *Client) Get(ctx context.Context, path string) (*http.Response, error) { func (c *Client) Delete(ctx context.Context, path string) (*http.Response, error) { return c.do(ctx, "DELETE", path, nil) } + + + + +type ListUnitsFastResponse struct { + Units []*UnitMetadata `json:"units"` + Count int `json:"count"` + Source string `json:"source"` +} + +// ListUnitsFast lists units using database (POC) +func (c *Client) ListUnitsFast(ctx context.Context, prefix string) (*ListUnitsFastResponse, error) { + path := "/v1/units-fast" + if prefix != "" { + path += "?prefix=" + url.QueryEscape(prefix) + } + + resp, err := c.do(ctx, "GET", path, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, parseError(resp) + } + + var result ListUnitsFastResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + return &result, nil +} From 0e3efe2006893a3f8f883c10ba74cc4abc72fc87 Mon Sep 17 00:00:00 2001 From: Brian Reardon Date: Mon, 29 Sep 2025 17:05:08 -0700 Subject: [PATCH 03/13] add lightstream example --- taco/configs/litestream.txt | 5 +++++ taco/configs/litestream.yml | 6 ++++++ 2 files changed, 11 insertions(+) create mode 100644 taco/configs/litestream.txt create mode 100644 taco/configs/litestream.yml diff --git a/taco/configs/litestream.txt b/taco/configs/litestream.txt new file mode 100644 index 000000000..ebae416ef --- /dev/null +++ b/taco/configs/litestream.txt @@ -0,0 +1,5 @@ + +#restore command + +litestream restore -o /Users/brianreardon/development/digger/taco/data/taco.db \ + s3://open-taco-brian/backups/taco.db \ No newline at end of file diff --git a/taco/configs/litestream.yml b/taco/configs/litestream.yml new file mode 100644 index 000000000..b92302af2 --- /dev/null +++ b/taco/configs/litestream.yml @@ -0,0 +1,6 @@ +dbs: + - path: /Users/brianreardon/development/digger/taco/data/taco.db + replicas: + - url: s3://open-taco-brian/backups/taco.db + region: us-east-2 + From 12ead85a923b7729588e1c9586f166e625d2b569 Mon Sep 17 00:00:00 2001 From: Brian Reardon Date: Fri, 3 Oct 2025 14:55:24 -0700 Subject: [PATCH 04/13] query wip --- taco/cmd/statesman/main.go | 29 ++- taco/internal/query/factory.go | 58 +++++ taco/internal/query/interface.go | 44 ++++ taco/internal/query/noop/store.go | 19 ++ taco/internal/query/postgres/store.go | 1 + taco/internal/query/sqlite/store.go | 332 ++++++++++++++++++++++++++ taco/internal/query/types/errors.go | 13 + taco/internal/query/types/models.go | 139 +++++++++++ taco/internal/unit/handler.go | 128 ++++++++-- 9 files changed, 732 insertions(+), 31 deletions(-) create mode 100644 taco/internal/query/factory.go create mode 100644 taco/internal/query/interface.go create mode 100644 taco/internal/query/noop/store.go create mode 100644 taco/internal/query/postgres/store.go create mode 100644 taco/internal/query/sqlite/store.go create mode 100644 taco/internal/query/types/errors.go create mode 100644 taco/internal/query/types/models.go diff --git a/taco/cmd/statesman/main.go b/taco/cmd/statesman/main.go index 3e6a32b99..097003cfc 100644 --- a/taco/cmd/statesman/main.go +++ b/taco/cmd/statesman/main.go @@ -38,7 +38,22 @@ func main() { ) flag.Parse() - database := db.OpenSQLite(db.DBConfig{}) // init db + //database := db.OpenSQLite(db.DBConfig{}) // init db + + queryStore, err := query.NewQueryStoreFromEnv() + if err != nil { + log.Fatalf("Failed to initialize query backend: %v", err) + } + + defer queryStore.Close() + + if queryStore.IsEnabled(){ + log.Println("Query backend enabled successfully") + }else{ + log.Println("Query backend disabled. You are in no-op mode.") + } + + // Initialize storage var store storage.UnitStore switch *storageType { @@ -61,11 +76,11 @@ func main() { //put on thread thread / adjust seed so it accepts any store // To this: - if s3Store, ok := store.(storage.S3Store); ok { - db.Seed(context.Background(), s3Store, database) - } else { - log.Println("Store is not S3Store, skipping seeding") - } + // if s3Store, ok := store.(storage.S3Store); ok { + // db.Seed(context.Background(), s3Store, database) + // } else { + // log.Println("Store is not S3Store, skipping seeding") + // } } default: store = storage.NewMemStore() @@ -104,7 +119,7 @@ func main() { e.Use(middleware.Secure()) e.Use(middleware.CORS()) - api.RegisterRoutes(e, store, !*authDisable, database) + api.RegisterRoutes(e, store, !*authDisable, queryStore) // Start server go func() { diff --git a/taco/internal/query/factory.go b/taco/internal/query/factory.go new file mode 100644 index 000000000..2fca23cf7 --- /dev/null +++ b/taco/internal/query/factory.go @@ -0,0 +1,58 @@ +package query + + +import ( + "os" + "strings" + "time" + "github.com/diggerhq/digger/opentaco/internal/query/sqlite" + "github.com/diggerhq/digger/opentaco/internal/query/noop" +) + + + +func NewQueryStoreFromEnv() (QueryStore, error) { + + backend := os.Getenv("TACO_QUERY_BACKEND") + backend = strings.ToLower(backend) // lowercase everythign + + + switch backend { + case "sqlite": + return newSQLiteFromEnv() + case "off": + return noop.NewNoOpQueryStore(), nil + default: + return newSQLiteFromEnv() + } +} + + +func newSQLiteFromEnv() (QueryStore, error) { + cfg := sqlite.Config{ + Path: getEnv("TACO_SQLITE_PATH", "./data/taco.db"), + Cache: getEnv("TACO_SQLITE_CACHE", "shared"), + EnableForeignKeys: getEnvBool("TACO_SQLITE_FOREIGN_KEYS", true), + EnableWAL: getEnvBool("TACO_SQLITE_WAL", true), + BusyTimeout: 5 * time.Second, + MaxOpenConns: 1, + MaxIdleConns: 1, + ConnMaxLifetime: 0, + } + + return sqlite.NewSQLiteQueryStore(cfg) +} + +func getEnv(key, defaultVal string) string { + if val := os.Getenv(key); val != "" { + return val + } + return defaultVal +} + +func getEnvBool(key string, defaultVal bool) bool { + if val := os.Getenv(key); val != "" { + return val == "true" || val == "1" + } + return defaultVal +} \ No newline at end of file diff --git a/taco/internal/query/interface.go b/taco/internal/query/interface.go new file mode 100644 index 000000000..bfd0d85cc --- /dev/null +++ b/taco/internal/query/interface.go @@ -0,0 +1,44 @@ +package query + + +import ( + "context" + "github.com/diggerhq/digger/opentaco/internal/query/types" +) + + +type QueryStore interface { + Close() error + IsEnabled() bool +} + + +type UnitQuery interface { + ListUnits(ctx context.Context, prefix string) ([]types.Unit,error) + GetUnit(ctx context.Context, id string) (*types.Unit, error) + SyncCreateUnit(ctx context.Context, unitName string) error + SyncDeleteUnit(ctx context.Context, unitName string) error + SyncUnitExists(ctx context.Context, unitName string) error +} + + +type RBACQuery interface { + FilterUnitIDsByUser(ctx context.Context, userSubject string, unitIDs []string) ([]string, error) + ListUnitsForUser(ctx context.Context, userSubject string, prefix string) ([]types.Unit, error) +} + + +func SupportsUnitQuery(store QueryStore) (UnitQuery, bool) { + q, ok := store.(UnitQuery) + + return q,ok + +} + + +func SupportsRBACQuery(store QueryStore) (RBACQuery, bool) { + q,ok := store.(RBACQuery) + + return q,ok +} + diff --git a/taco/internal/query/noop/store.go b/taco/internal/query/noop/store.go new file mode 100644 index 000000000..974584f4a --- /dev/null +++ b/taco/internal/query/noop/store.go @@ -0,0 +1,19 @@ +package noop + +// noop store +// basically allows graceful fallback if someone configures for no sqlite + +type NoOpQueryStore struct{} + +func NewNoOpQueryStore() *NoOpQueryStore { + return &NoOpQueryStore{} +} + +func (n *NoOpQueryStore) Close() error { + return nil +} + +func (n *NoOpQueryStore) IsEnabled() bool { + // Not NOOP ? + return false +} diff --git a/taco/internal/query/postgres/store.go b/taco/internal/query/postgres/store.go new file mode 100644 index 000000000..233b9ca00 --- /dev/null +++ b/taco/internal/query/postgres/store.go @@ -0,0 +1 @@ +package postgres \ No newline at end of file diff --git a/taco/internal/query/sqlite/store.go b/taco/internal/query/sqlite/store.go new file mode 100644 index 000000000..ac7efe0f5 --- /dev/null +++ b/taco/internal/query/sqlite/store.go @@ -0,0 +1,332 @@ +package sqlite + + +import ( + "os" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" + "path/filepath" + "fmt" + "context" + "log" + "time" + + + "github.com/diggerhq/digger/opentaco/internal/query/types" + +) + + +type SQLiteQueryStore struct { + db *gorm.DB + config Config +} + + +type Config struct { + Path string + Models []any + Cache string + EnableForeignKeys bool + EnableWAL bool + BusyTimeout time.Duration + MaxOpenConns int + MaxIdleConns int + ConnMaxLifetime time.Duration +} + +func NewSQLiteQueryStore(cfg Config) (*SQLiteQueryStore, error) { + + //set up SQLite + db, err := openSQLite(cfg) + + if err != nil { + + return nil, fmt.Errorf("Failed to open SQLite: %s", err) + } + + //initialize the store + store := &SQLiteQueryStore{db: db, config: cfg} + + + // migrate the models + if err := store.migrate(); err != nil { + return nil, fmt.Errorf("Failed to migrate store: %w", err) + } + + // create the views for the store + if err := store.createViews(); err != nil { + return nil, fmt.Errorf("Failed to create views for the store: %v", err) + } + + log.Printf("SQLite query store successfully initialized: %s", cfg.Path) + + + return store, nil +} + + + +func openSQLite(cfg Config) (*gorm.DB, error){ + + + if cfg.Path == "" { + cfg.Path = "./data/taco.db" + + if err := os.MkdirAll(filepath.Dir(cfg.Path), 0755); err != nil { + return nil, fmt.Errorf("create db dir: %v", err) + } + + } + + + if cfg.Cache == "" { + cfg.Cache = "shared" + } + + if cfg.BusyTimeout == 0 { + cfg.BusyTimeout = 5 * time.Second + } + + if cfg.MaxOpenConns == 0 { + cfg.MaxOpenConns = 1 + } + + if cfg.MaxIdleConns == 0 { + cfg.MaxIdleConns = 1 + } + + // (ConnMaxLifeTime default to 0) + + + dsn := fmt.Sprintf ("file:%s?cache=%v", cfg.Path, cfg.Cache) + + db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Info), // show SQL while developing + }) + if err != nil { + return nil , fmt.Errorf("open sqlite: %v", err) + } + + // Connection pool hints (SQLite is single-writer; 1 open conn is safe) + sqlDB, err := db.DB() + if err != nil { + return nil, fmt.Errorf("unwrap sql.DB: %w", err) + } + sqlDB.SetMaxOpenConns(cfg.MaxOpenConns) + sqlDB.SetMaxIdleConns(cfg.MaxIdleConns) + sqlDB.SetConnMaxLifetime(cfg.ConnMaxLifetime) + + // Helpful PRAGMAs + if err := db.Exec(` + PRAGMA journal_mode=WAL; + PRAGMA foreign_keys=ON; + PRAGMA busy_timeout=5000; + `).Error; err != nil { + return nil, fmt.Errorf("pragmas: %v", err) + } + + return db, nil + +} + + +func (s *SQLiteQueryStore) migrate() error { + + // expect default models + models := types.DefaultModels + + + // if the models are specified, load them + if len(s.config.Models) > 0 { + models = s.config.Models + } + + if err := s.db.AutoMigrate(models...); err != nil { + return fmt.Errorf("Migration failed: %w", err) + } + + return nil +} +func (s *SQLiteQueryStore) createViews() error { + + // cleaner way to abstract this ? + // Create the user-unit access view for fast lookups + if err := s.db.Exec(` + CREATE VIEW IF NOT EXISTS user_unit_access AS + WITH user_permissions AS ( + SELECT DISTINCT + u.subject as user_subject, + r.id as rule_id, + r.wildcard_resource, + r.effect + FROM users u + JOIN user_roles ur ON u.id = ur.user_id + JOIN role_permissions rp ON ur.role_id = rp.role_id + JOIN rules r ON rp.permission_id = r.permission_id + LEFT JOIN rule_actions ra ON r.id = ra.rule_id + WHERE r.effect = 'allow' + AND (r.wildcard_action = 1 OR ra.action = 'unit.read' OR ra.action IS NULL) + ), + wildcard_access AS ( + SELECT DISTINCT + up.user_subject, + un.name as unit_name + FROM user_permissions up + CROSS JOIN units un + WHERE up.wildcard_resource = 1 + ), + specific_access AS ( + SELECT DISTINCT + up.user_subject, + un.name as unit_name + FROM user_permissions up + JOIN rule_units ru ON up.rule_id = ru.rule_id + JOIN units un ON ru.unit_id = un.id + WHERE up.wildcard_resource = 0 + ) + SELECT user_subject, unit_name FROM wildcard_access + UNION + SELECT user_subject, unit_name FROM specific_access; + `).Error; err != nil { + log.Printf("Warning: failed to create user_unit_access view: %v", err) + } + + + + + + + return nil +} + +// prefix is the location within the bucket like /prod/region1/etc +func (s *SQLiteQueryStore) ListUnits(ctx context.Context, prefix string) ([]types.Unit, error) { + var units []types.Unit + q := s.db.WithContext(ctx).Preload("Tags") + + if prefix != "" { + q = q.Where("name LIKE ?", prefix+"%") + } + + if err := q.Find(&units).Error; err != nil { + return nil, err + } + + return units, nil +} + +func (s *SQLiteQueryStore) GetUnit(ctx context.Context, id string) (*types.Unit, error) { + var unit types.Unit + err := s.db.WithContext(ctx). + Preload("Tags"). + Where("name = ?", id). + First(&unit).Error + + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, types.ErrNotFound + } + return nil, err + } + + return &unit, nil +} + + + + +func (s *SQLiteQueryStore) IsEnabled() bool{ + // not NOOP ? + return true +} + +func (s *SQLiteQueryStore) Close() error{ + sqlDB, err := s.db.DB() + if err != nil { + return err + } + return sqlDB.Close() +} + + + +func (s *SQLiteQueryStore) SyncCreateUnit(ctx context.Context, unitName string) error { + unit := types.Unit{Name: unitName} + return s.db.WithContext(ctx).FirstOrCreate(&unit, types.Unit{Name: unitName}).Error +} + +func (s *SQLiteQueryStore) SyncDeleteUnit(ctx context.Context, unitName string) error { + return s.db.WithContext(ctx).Where("name = ?", unitName).Delete(&types.Unit{}).Error +} + +func (s *SQLiteQueryStore) SyncUnitExists(ctx context.Context, unitName string) error { + unit := types.Unit{Name: unitName} + return s.db.WithContext(ctx).FirstOrCreate(&unit, types.Unit{Name: unitName}).Error +} + + + + +func (s *SQLiteQueryStore) FilterUnitIDsByUser(ctx context.Context, userSubject string, unitIDs []string) ([]string, error){ + +// empty input? + if len(unitIDs) == 0 { + return []string{}, nil + } + + + var allowedUnitIDs []string + + + + err := s.db.WithContext(ctx). + Table("user_unit_access"). + Select("unit_name"). + Where ("user_subject = ?", userSubject). + Where("unit_name IN ?", unitIDs). + Pluck("unit_name", &allowedUnitIDs).Error + + + if err != nil { + return nil, fmt.Errorf("Failed to filter the units by user : %w", err) + + } + + return allowedUnitIDs, nil +} + + + + +func (s *SQLiteQueryStore) ListUnitsForUser(ctx context.Context, userSubject string, prefix string) ([]types.Unit, error) { + var units []types.Unit + + q := s.db.WithContext(ctx). + Table("units"). + Select("units.*"). + Joins("JOIN user_unit_access ON units.name = user_unit_access.unit_name"). + Where("user_unit_access.user_subject = ?", userSubject). + Preload("Tags") + + if prefix != "" { + q = q.Where("units.name LIKE ?", prefix +"%") + } + + + + err:= q.Find(&units).Error + + if err != nil { + return nil, fmt.Errorf("failed to list units for user: %w", err) + } + + return units, nil +} + + + + + + diff --git a/taco/internal/query/types/errors.go b/taco/internal/query/types/errors.go new file mode 100644 index 000000000..4956012fe --- /dev/null +++ b/taco/internal/query/types/errors.go @@ -0,0 +1,13 @@ +package types + +import ( + + "errors" +) + + + +var ( + ErrNotSupported = errors.New("Query operation not supported by this backend") + ErrNotFound = errors.New("Not found") +) diff --git a/taco/internal/query/types/models.go b/taco/internal/query/types/models.go new file mode 100644 index 000000000..5dcee31e0 --- /dev/null +++ b/taco/internal/query/types/models.go @@ -0,0 +1,139 @@ +package types + + +import ( + "time" +) + + +type Role struct { + ID int64 `gorm:"primaryKey"` + RoleId string `gorm:"not null;uniqueIndex"`// like "admin" + Name string //" admin role" + Description string // "Admin Role with full access" + Permissions []Permission `gorm:"many2many:role_permissions;constraint:OnDelete:CASCADE,OnUpdate:CASCADE"` + CreatedAt time.Time//timestamp + CreatedBy string //subject of creator (self for admin) +} + + + + +type Permission struct { + ID int64 `gorm:"primaryKey"` + PermissionId string `gorm:"not null;uniqueIndex"` + Name string // "admin permission" + Description string // "Admin permission allowing all action" + Rules []Rule `gorm:"constraint:OnDelete:CASCADE"` // [{"actions":["unit.read","unit.write","unit.lock","unit.delete","rbac.manage"],"resources":["*"],"effect":"allow"}] FK + CreatedBy string // subject of creator (self for admin) + CreatedAt time.Time +} + +type Rule struct { + ID int64 `gorm:"primaryKey"` + PermissionID int64 `gorm:"index;not null"` + Effect string `gorm:"size:8;not null;default:allow"` // "allow" | "deny" + WildcardAction bool `gorm:"not null;default:false"` + WildcardResource bool `gorm:"not null;default:false"` + Actions []RuleAction `gorm:"constraint:OnDelete:CASCADE"` + UnitTargets []RuleUnit `gorm:"constraint:OnDelete:CASCADE"` + TagTargets []RuleUnitTag `gorm:"constraint:OnDelete:CASCADE"` +} + + + +type RuleAction struct { + ID int64 `gorm:"primaryKey"` + RuleID int64 `gorm:"index;not null"` + Action string `gorm:"size:128;not null;index"` + // UNIQUE (rule_id, action) +} +func (RuleAction) TableName() string { return "rule_actions" } + +type RuleUnit struct { + ID int64 `gorm:"primaryKey"` + RuleID int64 `gorm:"index;not null"` + UnitID int64 `gorm:"index;not null"` + // UNIQUE (rule_id, resource_id) +} +func (RuleUnit) TableName() string { return "rule_units" } + +type RuleUnitTag struct { + ID int64 `gorm:"primaryKey"` + RuleID int64 `gorm:"index;not null"` + TagID int64 `gorm:"index;not null"` + // UNIQUE (rule_id, tag_id) +} +func (RuleUnitTag) TableName() string { return "rule_unit_tags" } + + + + +type User struct { + ID int64 `gorm:"primaryKey"` + Subject string `gorm:"not null;uniqueIndex"` + Email string `gorm:"not nulll;uniqueIndex"` + Roles []Role `gorm:"many2many:user_roles;constraint:OnDelete:CASCADE,OnUpdate:CASCADE"` + CreatedAt time.Time + UpdatedAt time.Time + Version int64 //"1" +} + +type Unit struct { + ID int64 `gorm:"primaryKey"` + Name string `gorm:"uniqueIndex"` + Tags []Tag `gorm:"many2many:unit_tags;constraint:OnDelete:CASCADE,OnUpdate:CASCADE"` + +} + +type Tag struct { + ID int64 `gorm:"primaryKey"` + Name string `gorm:"uniqueIndex"` + +} + + +//explicit joins + + +type UnitTag struct { + UnitID int64 `gorm:"primaryKey;index"` + TagID int64 `gorm:"primaryKey;index"` +} +func (UnitTag) TableName() string { return "unit_tags" } + + +type UserRole struct { + UserID int64 `gorm:"primaryKey;index"` + RoleID int64 `gorm:"primaryKey;index"` +} +func (UserRole) TableName() string { return "user_roles" } + + + +type RolePermission struct { + RoleID int64 `gorm:"primaryKey;index"` + PermissionID int64 `gorm:"primaryKey;index"` +} + + +func (RolePermission) TableName() string { return "role_permissions" } + + + + +// set the models that will be populated on startup for each DB type; add any new tables here: +var DefaultModels = []any{ + &User{}, + &Role{}, + &UserRole{}, + &Permission{}, + &Rule{}, + &RuleAction{}, + &RuleUnit{}, + &RuleUnitTag{}, + &RolePermission{}, + &Unit{}, + &Tag{}, + &UnitTag{}, +} \ No newline at end of file diff --git a/taco/internal/unit/handler.go b/taco/internal/unit/handler.go index 33331240c..30cc15f93 100644 --- a/taco/internal/unit/handler.go +++ b/taco/internal/unit/handler.go @@ -13,10 +13,12 @@ import ( "github.com/diggerhq/digger/opentaco/internal/rbac" "github.com/diggerhq/digger/opentaco/internal/storage" "github.com/diggerhq/digger/opentaco/internal/db" + "github.com/diggerhq/digger/opentaco/internal/query" "gorm.io/gorm" "github.com/google/uuid" "github.com/labstack/echo/v4" "log" + "context" ) @@ -82,52 +84,130 @@ func (h *Handler) CreateUnit(c echo.Context) error { } func (h *Handler) ListUnits(c echo.Context) error { + ctx := c.Request().Context() prefix := c.QueryParam("prefix") - items, err := h.store.List(c.Request().Context(), prefix) + + + if unitQuery, ok := SupportsUnitQuery(h.queryStore); ok { + rbacQuery, hasRBAC := SupportsRBACQuery(h.queryStore) + + units, err := unitQuery.ListUnits(ctx, prefix) + if err == nil { + // Index by ID + unitIDs := make([]string, len(units)) + unitMap := make(map[string]query.Unit, len(units)) + for i, u := range units { + unitIDs[i] = u.Name + unitMap[u.Name] = u + } + + if hasRBAC && h.rbacManager != nil && h.signer != nil { + principal, perr := h.getPrincipalFromToken(c) + if perr != nil { + // If RBAC is enabled, return 401; otherwise skip RBAC + if enabled, _ := h.rbacManager.IsEnabled(ctx); enabled { + return c.JSON(http.StatusUnauthorized, map[string]string{ + "error": "Failed to authenticate user", + }) + } + } else { + filteredIDs, ferr := rbacQuery.FilterUnitIDsByUser(ctx, principal.Subject, unitIDs) + if ferr != nil { + log.Printf("RBAC filtering via query-store failed; falling back to storage: %v", ferr) + return h.listFromStorage(ctx, c, prefix) + } + unitIDs = filteredIDs + } + } + + // Build response from filtered IDs + domainUnits := make([]*domain.Unit, 0, len(unitIDs)) + for _, id := range unitIDs { + if u, ok := unitMap[id]; ok { + tagNames := make([]string, len(u.Tags)) + for i, t := range u.Tags { + tagNames[i] = t.Name + } + domainUnits = append(domainUnits, &domain.Unit{ + ID: u.Name, + Tags: tagNames, + }) + } + } + domain.SortUnitsByID(domainUnits) + + return c.JSON(http.StatusOK, map[string]interface{}{ + "units": domainUnits, + "count": len(domainUnits), + "source": "query-store", + }) + } + + log.Printf("Query-store ListUnits failed; falling back to storage: %v", err) + } + + return h.listFromStorage(ctx, c, prefix) +} + +// listFromStorage encapsulates the old storage-based path (including RBAC). +func (h *Handler) listFromStorage(ctx context.Context, c echo.Context, prefix string) error { + items, err := h.store.List(ctx, prefix) if err != nil { - return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to list units"}) + return c.JSON(http.StatusInternalServerError, map[string]string{ + "error": "Failed to list units", + }) } - var units []*domain.Unit unitIDs := make([]string, 0, len(items)) - unitMap := make(map[string]*storage.UnitMetadata) - - // Collect all unit IDs and create a map for quick lookup + unitMap := make(map[string]*storage.UnitMetadata, len(items)) for _, s := range items { unitIDs = append(unitIDs, s.ID) unitMap[s.ID] = s } - // Filter units by RBAC permissions if available + // Storage-based RBAC (manager-driven) if h.rbacManager != nil && h.signer != nil { - principal, err := h.getPrincipalFromToken(c) - if err != nil { - // If we can't get principal, check if RBAC is enabled - if enabled, _ := h.rbacManager.IsEnabled(c.Request().Context()); enabled { - return c.JSON(http.StatusUnauthorized, map[string]string{"error": "Failed to authenticate user"}) + principal, perr := h.getPrincipalFromToken(c) + if perr != nil { + if enabled, _ := h.rbacManager.IsEnabled(ctx); enabled { + return c.JSON(http.StatusUnauthorized, map[string]string{ + "error": "Failed to authenticate user", + }) } - // RBAC not enabled, show all units + // RBAC not enabled -> show all units } else { - // Filter units based on read permissions - filteredIDs, err := h.rbacManager.FilterUnitsByReadAccess(c.Request().Context(), principal, unitIDs) - if err != nil { - return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to check permissions"}) + filtered, ferr := h.rbacManager.FilterUnitsByReadAccess(ctx, principal, unitIDs) + if ferr != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{ + "error": "Failed to check permissions", + }) } - unitIDs = filteredIDs + unitIDs = filtered } } - // Build response with filtered units + // Build response + out := make([]*domain.Unit, 0, len(unitIDs)) for _, id := range unitIDs { - if s, exists := unitMap[id]; exists { - units = append(units, &domain.Unit{ID: s.ID, Size: s.Size, Updated: s.Updated, Locked: s.Locked, LockInfo: convertLockInfo(s.LockInfo)}) + if s, ok := unitMap[id]; ok { + out = append(out, &domain.Unit{ + ID: s.ID, + Size: s.Size, + Updated: s.Updated, + Locked: s.Locked, + LockInfo: convertLockInfo(s.LockInfo), + }) } } - - domain.SortUnitsByID(units) - return c.JSON(http.StatusOK, map[string]interface{}{"units": units, "count": len(units)}) + domain.SortUnitsByID(out) + + return c.JSON(http.StatusOK, map[string]interface{}{ + "units": out, + "count": len(out), + }) } + func (h *Handler) GetUnit(c echo.Context) error { encodedID := c.Param("id") id := domain.DecodeUnitID(encodedID) From f7868f8ddb8b431f4c78900f3aecf73fff49b065 Mon Sep 17 00:00:00 2001 From: Brian Reardon Date: Fri, 3 Oct 2025 19:05:53 -0700 Subject: [PATCH 05/13] EOD wip --- go.mod | 1 + go.sum | 2 + taco/cmd/statesman/main.go | 15 ++- taco/internal/api/routes.go | 8 +- taco/internal/domain/unit.go | 1 + taco/internal/query/config.go | 22 ++++ taco/internal/query/factory.go | 74 ++++-------- taco/internal/query/interface.go | 29 ++--- taco/internal/query/noop/store.go | 40 ++++++- taco/internal/query/sqlite/store.go | 170 +++++++++++++++++----------- taco/internal/query/types/models.go | 2 +- taco/internal/unit/handler.go | 163 +++++++++++++------------- 12 files changed, 293 insertions(+), 234 deletions(-) create mode 100644 taco/internal/query/config.go diff --git a/go.mod b/go.mod index 546539d76..0df54e17b 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.24.0 require ( github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect + github.com/kelseyhightower/envconfig v1.4.0 // indirect github.com/mattn/go-sqlite3 v1.14.22 // indirect golang.org/x/text v0.20.0 // indirect gorm.io/driver/sqlite v1.6.0 // indirect diff --git a/go.sum b/go.sum index 79b84992a..db53b015e 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= +github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= 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= golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= diff --git a/taco/cmd/statesman/main.go b/taco/cmd/statesman/main.go index 097003cfc..762a11958 100644 --- a/taco/cmd/statesman/main.go +++ b/taco/cmd/statesman/main.go @@ -21,8 +21,9 @@ import ( "github.com/diggerhq/digger/opentaco/internal/analytics" "github.com/diggerhq/digger/opentaco/internal/api" + "github.com/diggerhq/digger/opentaco/internal/query" "github.com/diggerhq/digger/opentaco/internal/storage" - "github.com/diggerhq/digger/opentaco/internal/db" + "github.com/kelseyhightower/envconfig" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" ) @@ -38,10 +39,16 @@ func main() { ) flag.Parse() - //database := db.OpenSQLite(db.DBConfig{}) // init db + // Load configuration from environment variables into our struct. + var queryCfg query.Config + err := envconfig.Process("taco", &queryCfg) // The prefix "TACO" will be used for all vars. + if err != nil { + log.Fatalf("Failed to process configuration: %v", err) + } - queryStore, err := query.NewQueryStoreFromEnv() - if err != nil { + // Pass the populated config struct to the factory. + queryStore, err := query.NewQueryStore(queryCfg) + if err != nil { log.Fatalf("Failed to initialize query backend: %v", err) } diff --git a/taco/internal/api/routes.go b/taco/internal/api/routes.go index 09d4cd7a1..5892f8ff6 100644 --- a/taco/internal/api/routes.go +++ b/taco/internal/api/routes.go @@ -20,12 +20,12 @@ import ( "github.com/diggerhq/digger/opentaco/internal/oidc" "github.com/diggerhq/digger/opentaco/internal/sts" "github.com/diggerhq/digger/opentaco/internal/storage" + "github.com/diggerhq/digger/opentaco/internal/query" "github.com/labstack/echo/v4" - "gorm.io/gorm" ) // RegisterRoutes registers all API routes -func RegisterRoutes(e *echo.Echo, store storage.UnitStore, authEnabled bool, db *gorm.DB) { +func RegisterRoutes(e *echo.Echo, store storage.UnitStore, authEnabled bool, queryStore query.QueryStore) { // Health checks health := observability.NewHealthHandler() e.GET("/healthz", health.Healthz) @@ -147,14 +147,14 @@ func RegisterRoutes(e *echo.Echo, store storage.UnitStore, authEnabled bool, db } // Unit handlers (management API) - pass RBAC manager and signer for filtering - unitHandler := unithandlers.NewHandler(store, rbacManager, signer, db) + unitHandler := unithandlers.NewHandler(store, rbacManager, signer, queryStore) // Management API (units) with RBAC middleware if authEnabled && rbacManager != nil { v1.POST("/units", middleware.RBACMiddleware(rbacManager, signer, rbac.ActionUnitWrite, "*")(unitHandler.CreateUnit)) // ListUnits does its own RBAC filtering internally, no middleware needed v1.GET("/units", unitHandler.ListUnits) - v1.GET("/units-fast", unitHandler.ListUnitsFast) + // v1.GET("/units-fast", unitHandler.ListUnitsFast) v1.GET("/units/:id", middleware.RBACMiddleware(rbacManager, signer, rbac.ActionUnitRead, "{id}")(unitHandler.GetUnit)) v1.DELETE("/units/:id", middleware.RBACMiddleware(rbacManager, signer, rbac.ActionUnitDelete, "{id}")(unitHandler.DeleteUnit)) v1.GET("/units/:id/download", middleware.RBACMiddleware(rbacManager, signer, rbac.ActionUnitRead, "{id}")(unitHandler.DownloadUnit)) diff --git a/taco/internal/domain/unit.go b/taco/internal/domain/unit.go index 7f7e26a2a..a9b6267e7 100644 --- a/taco/internal/domain/unit.go +++ b/taco/internal/domain/unit.go @@ -13,6 +13,7 @@ type Unit struct { Updated time.Time `json:"updated"` Locked bool `json:"locked"` LockInfo *Lock `json:"lock,omitempty"` + Tags []string `json:"tags,omitempty"` } // Lock represents lock information for a unit diff --git a/taco/internal/query/config.go b/taco/internal/query/config.go new file mode 100644 index 000000000..6f9602485 --- /dev/null +++ b/taco/internal/query/config.go @@ -0,0 +1,22 @@ +package query + +import "time" + +// Config holds all configuration for the query store, loaded from environment variables. +type Config struct { + Backend string `envconfig:"QUERY_BACKEND" default:"sqlite"` + SQLite SQLiteConfig `envconfig:"SQLITE"` + // Postgres PostgresConfig `envconfig:"POSTGRES"` +} + +// SQLiteConfig holds all the specific settings for the SQLite backend. +type SQLiteConfig struct { + Path string `envconfig:"PATH" default:"./data/taco.db"` + Cache string `envconfig:"CACHE" default:"shared"` + BusyTimeout time.Duration `envconfig:"BUSY_TIMEOUT" default:"5s"` + MaxOpenConns int `envconfig:"MAX_OPEN_CONNS" default:"1"` + MaxIdleConns int `envconfig:"MAX_IDLE_CONNS" default:"1"` + PragmaJournalMode string `envconfig:"PRAGMA_JOURNAL_MODE" default:"WAL"` + PragmaForeignKeys string `envconfig:"PRAGMA_FOREIGN_KEYS" default:"ON"` + PragmaBusyTimeout string `envconfig:"PRAGMA_BUSY_TIMEOUT" default:"5000"` +} \ No newline at end of file diff --git a/taco/internal/query/factory.go b/taco/internal/query/factory.go index 2fca23cf7..7b34fed00 100644 --- a/taco/internal/query/factory.go +++ b/taco/internal/query/factory.go @@ -1,58 +1,34 @@ -package query +package query - -import ( - "os" +import ( + "fmt" "strings" - "time" - "github.com/diggerhq/digger/opentaco/internal/query/sqlite" + "github.com/diggerhq/digger/opentaco/internal/query/noop" + "github.com/diggerhq/digger/opentaco/internal/query/sqlite" ) - - -func NewQueryStoreFromEnv() (QueryStore, error) { - - backend := os.Getenv("TACO_QUERY_BACKEND") - backend = strings.ToLower(backend) // lowercase everythign - +// NewQueryStore creates a new query.Store based on the provided configuration. +func NewQueryStore(cfg Config) (Store, error) { + backend := strings.ToLower(cfg.Backend) switch backend { - case "sqlite": - return newSQLiteFromEnv() - case "off": - return noop.NewNoOpQueryStore(), nil - default: - return newSQLiteFromEnv() + case "sqlite", "": + // Map our config struct to the one sqlite's New function expects. + sqliteCfg := sqlite.Config{ + Path: cfg.SQLite.Path, + Cache: cfg.SQLite.Cache, + BusyTimeout: cfg.SQLite.BusyTimeout, + MaxOpenConns: cfg.SQLite.MaxOpenConns, + MaxIdleConns: cfg.SQLite.MaxIdleConns, + PragmaJournalMode: cfg.SQLite.PragmaJournalMode, + PragmaForeignKeys: cfg.SQLite.PragmaForeignKeys, + PragmaBusyTimeout: cfg.SQLite.PragmaBusyTimeout, + } + return sqlite.NewSQLiteQueryStore(sqliteCfg) + case "off": + return noop.NewNoOpQueryStore(), nil + default: + return nil, fmt.Errorf("unsupported TACO_QUERY_BACKEND value: %q", backend) } -} - - -func newSQLiteFromEnv() (QueryStore, error) { - cfg := sqlite.Config{ - Path: getEnv("TACO_SQLITE_PATH", "./data/taco.db"), - Cache: getEnv("TACO_SQLITE_CACHE", "shared"), - EnableForeignKeys: getEnvBool("TACO_SQLITE_FOREIGN_KEYS", true), - EnableWAL: getEnvBool("TACO_SQLITE_WAL", true), - BusyTimeout: 5 * time.Second, - MaxOpenConns: 1, - MaxIdleConns: 1, - ConnMaxLifetime: 0, - } - - return sqlite.NewSQLiteQueryStore(cfg) -} - -func getEnv(key, defaultVal string) string { - if val := os.Getenv(key); val != "" { - return val - } - return defaultVal -} - -func getEnvBool(key string, defaultVal bool) bool { - if val := os.Getenv(key); val != "" { - return val == "true" || val == "1" - } - return defaultVal } \ No newline at end of file diff --git a/taco/internal/query/interface.go b/taco/internal/query/interface.go index bfd0d85cc..6188e4a98 100644 --- a/taco/internal/query/interface.go +++ b/taco/internal/query/interface.go @@ -1,44 +1,31 @@ package query - import ( "context" "github.com/diggerhq/digger/opentaco/internal/query/types" ) - type QueryStore interface { Close() error - IsEnabled() bool + IsEnabled() bool } - type UnitQuery interface { - ListUnits(ctx context.Context, prefix string) ([]types.Unit,error) + ListUnits(ctx context.Context, prefix string) ([]types.Unit, error) GetUnit(ctx context.Context, id string) (*types.Unit, error) - SyncCreateUnit(ctx context.Context, unitName string) error - SyncDeleteUnit(ctx context.Context, unitName string) error - SyncUnitExists(ctx context.Context, unitName string) error + SyncEnsureUnit(ctx context.Context, unitName string) error + SyncDeleteUnit(ctx context.Context, unitName string) error } - type RBACQuery interface { FilterUnitIDsByUser(ctx context.Context, userSubject string, unitIDs []string) ([]string, error) ListUnitsForUser(ctx context.Context, userSubject string, prefix string) ([]types.Unit, error) } - -func SupportsUnitQuery(store QueryStore) (UnitQuery, bool) { - q, ok := store.(UnitQuery) - - return q,ok - +type Store interface { + QueryStore + UnitQuery + RBACQuery } -func SupportsRBACQuery(store QueryStore) (RBACQuery, bool) { - q,ok := store.(RBACQuery) - - return q,ok -} - diff --git a/taco/internal/query/noop/store.go b/taco/internal/query/noop/store.go index 974584f4a..82bc6008d 100644 --- a/taco/internal/query/noop/store.go +++ b/taco/internal/query/noop/store.go @@ -1,19 +1,47 @@ package noop -// noop store -// basically allows graceful fallback if someone configures for no sqlite +import ( + "context" + "errors" + "github.com/diggerhq/digger/opentaco/internal/query/types" +) + +// NoOpQueryStore provides a disabled query backend that satisfies the Store interface. type NoOpQueryStore struct{} func NewNoOpQueryStore() *NoOpQueryStore { - return &NoOpQueryStore{} + return &NoOpQueryStore{} } func (n *NoOpQueryStore) Close() error { - return nil + return nil } func (n *NoOpQueryStore) IsEnabled() bool { - // Not NOOP ? - return false + return false +} + +var errDisabled = errors.New("query store is disabled") + +// UnitQuery implementation (no-op) +func (n *NoOpQueryStore) ListUnits(ctx context.Context, prefix string) ([]types.Unit, error) { + return nil, errDisabled +} +func (n *NoOpQueryStore) GetUnit(ctx context.Context, id string) (*types.Unit, error) { + return nil, errDisabled +} +func (n *NoOpQueryStore) SyncEnsureUnit(ctx context.Context, unitName string) error { + return errDisabled +} +func (n *NoOpQueryStore) SyncDeleteUnit(ctx context.Context, unitName string) error { + return errDisabled +} + +// RBACQuery implementation (no-op) +func (n *NoOpQueryStore) FilterUnitIDsByUser(ctx context.Context, userSubject string, unitIDs []string) ([]string, error) { + return nil, errDisabled +} +func (n *NoOpQueryStore) ListUnitsForUser(ctx context.Context, userSubject string, prefix string) ([]types.Unit, error) { + return nil, errDisabled } diff --git a/taco/internal/query/sqlite/store.go b/taco/internal/query/sqlite/store.go index ac7efe0f5..b2e24fbd8 100644 --- a/taco/internal/query/sqlite/store.go +++ b/taco/internal/query/sqlite/store.go @@ -94,7 +94,20 @@ func openSQLite(cfg Config) (*gorm.DB, error){ } if cfg.MaxIdleConns == 0 { - cfg.MaxIdleConns = 1 + cfg.MaxIdleConns = 1 + } + + + if cfg.PragmaJournalMode == ""{ + cfg.PragmaJournalMode = "WAL" + } + + if cfg.PragmaForeignKeys == "" { + cfg.PragmaForeignKeys = "ON" + } + + if cfg.PragmaBusyTimeout == "" { + cfg.PragmaBusyTimeout = "5000" } // (ConnMaxLifeTime default to 0) @@ -118,13 +131,17 @@ func openSQLite(cfg Config) (*gorm.DB, error){ sqlDB.SetMaxIdleConns(cfg.MaxIdleConns) sqlDB.SetConnMaxLifetime(cfg.ConnMaxLifetime) - // Helpful PRAGMAs - if err := db.Exec(` - PRAGMA journal_mode=WAL; - PRAGMA foreign_keys=ON; - PRAGMA busy_timeout=5000; - `).Error; err != nil { - return nil, fmt.Errorf("pragmas: %v", err) + + if err := db.Exec(fmt.Sprintf("PRAGMA journal_mode = %s;", strings.ToUpper(cfg.PragmaJournalMode))).Error; err != nil { + return nil, fmt.Errorf("apply journal_mode: %w", err) + } + + if err := db.Exec(fmt.Sprintf("PRAGMA foreign_keys = %s;", strings.ToUpper(cfg.PragmaForeignKeys))).Error; err != nil { + return nil, fmt.Errorf("apply foreign_keys: %w", err) + } + + if err := db.Exec(fmt.Sprintf("PRAGMA busy_timeout = %s;", cfg.PragmaBusyTimeout)).Error; err != nil { + return nil, fmt.Errorf("apply busy_timeout: %w", err) } return db, nil @@ -149,57 +166,7 @@ func (s *SQLiteQueryStore) migrate() error { return nil } -func (s *SQLiteQueryStore) createViews() error { - // cleaner way to abstract this ? - // Create the user-unit access view for fast lookups - if err := s.db.Exec(` - CREATE VIEW IF NOT EXISTS user_unit_access AS - WITH user_permissions AS ( - SELECT DISTINCT - u.subject as user_subject, - r.id as rule_id, - r.wildcard_resource, - r.effect - FROM users u - JOIN user_roles ur ON u.id = ur.user_id - JOIN role_permissions rp ON ur.role_id = rp.role_id - JOIN rules r ON rp.permission_id = r.permission_id - LEFT JOIN rule_actions ra ON r.id = ra.rule_id - WHERE r.effect = 'allow' - AND (r.wildcard_action = 1 OR ra.action = 'unit.read' OR ra.action IS NULL) - ), - wildcard_access AS ( - SELECT DISTINCT - up.user_subject, - un.name as unit_name - FROM user_permissions up - CROSS JOIN units un - WHERE up.wildcard_resource = 1 - ), - specific_access AS ( - SELECT DISTINCT - up.user_subject, - un.name as unit_name - FROM user_permissions up - JOIN rule_units ru ON up.rule_id = ru.rule_id - JOIN units un ON ru.unit_id = un.id - WHERE up.wildcard_resource = 0 - ) - SELECT user_subject, unit_name FROM wildcard_access - UNION - SELECT user_subject, unit_name FROM specific_access; - `).Error; err != nil { - log.Printf("Warning: failed to create user_unit_access view: %v", err) - } - - - - - - - return nil -} // prefix is the location within the bucket like /prod/region1/etc func (s *SQLiteQueryStore) ListUnits(ctx context.Context, prefix string) ([]types.Unit, error) { @@ -237,12 +204,12 @@ func (s *SQLiteQueryStore) GetUnit(ctx context.Context, id string) (*types.Unit, -func (s *SQLiteQueryStore) IsEnabled() bool{ +func (s *SQLiteQueryStore) IsEnabled() bool { // not NOOP ? return true } -func (s *SQLiteQueryStore) Close() error{ +func (s *SQLiteQueryStore) Close() error { sqlDB, err := s.db.DB() if err != nil { return err @@ -252,7 +219,7 @@ func (s *SQLiteQueryStore) Close() error{ -func (s *SQLiteQueryStore) SyncCreateUnit(ctx context.Context, unitName string) error { +func (s *SQLiteQueryStore) SyncEnsureUnit(ctx context.Context, unitName string) error { unit := types.Unit{Name: unitName} return s.db.WithContext(ctx).FirstOrCreate(&unit, types.Unit{Name: unitName}).Error } @@ -261,15 +228,12 @@ func (s *SQLiteQueryStore) SyncDeleteUnit(ctx context.Context, unitName string) return s.db.WithContext(ctx).Where("name = ?", unitName).Delete(&types.Unit{}).Error } -func (s *SQLiteQueryStore) SyncUnitExists(ctx context.Context, unitName string) error { - unit := types.Unit{Name: unitName} - return s.db.WithContext(ctx).FirstOrCreate(&unit, types.Unit{Name: unitName}).Error -} -func (s *SQLiteQueryStore) FilterUnitIDsByUser(ctx context.Context, userSubject string, unitIDs []string) ([]string, error){ + +func (s *SQLiteQueryStore) FilterUnitIDsByUser(ctx context.Context, userSubject string, unitIDs []string) ([]string, error) { // empty input? if len(unitIDs) == 0 { @@ -326,7 +290,81 @@ func (s *SQLiteQueryStore) ListUnitsForUser(ctx context.Context, userSubject str } +type viewDefinition struct { + name string + sql string +} +// CTE method for user_permissions - gets users with their permission rules +func (s *SQLiteQueryStore) userPermissionsCTE() string { + return ` + SELECT DISTINCT + u.subject as user_subject, + r.id as rule_id, + r.wildcard_resource, + r.effect + FROM users u + JOIN user_roles ur ON u.id = ur.user_id + JOIN role_permissions rp ON ur.role_id = rp.role_id + JOIN rules r ON rp.permission_id = r.permission_id + LEFT JOIN rule_actions ra ON r.id = ra.rule_id + WHERE r.effect = 'allow' + AND (r.wildcard_action = 1 OR ra.action = 'unit.read' OR ra.action IS NULL)` +} + +// CTE method for wildcard_access - handles users with wildcard resource access +func (s *SQLiteQueryStore) wildcardAccessCTE() string { + return ` + SELECT DISTINCT + up.user_subject, + un.name as unit_name + FROM user_permissions up + CROSS JOIN units un + WHERE up.wildcard_resource = 1` +} + +// CTE method for specific_access - handles users with specific unit access +func (s *SQLiteQueryStore) specificAccessCTE() string { + return ` + SELECT DISTINCT + up.user_subject, + un.name as unit_name + FROM user_permissions up + JOIN rule_units ru ON up.rule_id = ru.rule_id + JOIN units un ON ru.unit_id = un.id + WHERE up.wildcard_resource = 0` +} +// Helper method to create individual views +func (s *SQLiteQueryStore) createView(name, sql string) error { + query := fmt.Sprintf("CREATE VIEW IF NOT EXISTS %s AS %s", name, sql) + return s.db.Exec(query).Error +} +// Refactored createViews method +func (s *SQLiteQueryStore) createViews() error { + views := []viewDefinition{ + { + name: "user_unit_access", + sql: s.buildUserUnitAccessView(), + }, + } + + for _, view := range views { + if err := s.createView(view.name, view.sql); err != nil { + return fmt.Errorf("failed to create view %s: %w", view.name, err) + } + } + return nil +} +// Refactored buildUserUnitAccessView method +func (s *SQLiteQueryStore) buildUserUnitAccessView() string { + return ` + WITH user_permissions AS (` + s.userPermissionsCTE() + `), + wildcard_access AS (` + s.wildcardAccessCTE() + `), + specific_access AS (` + s.specificAccessCTE() + `) + SELECT user_subject, unit_name FROM wildcard_access + UNION + SELECT user_subject, unit_name FROM specific_access` +} diff --git a/taco/internal/query/types/models.go b/taco/internal/query/types/models.go index 5dcee31e0..865f06de6 100644 --- a/taco/internal/query/types/models.go +++ b/taco/internal/query/types/models.go @@ -72,7 +72,7 @@ func (RuleUnitTag) TableName() string { return "rule_unit_tags" } type User struct { ID int64 `gorm:"primaryKey"` Subject string `gorm:"not null;uniqueIndex"` - Email string `gorm:"not nulll;uniqueIndex"` + Email string `gorm:"not null;uniqueIndex"` Roles []Role `gorm:"many2many:user_roles;constraint:OnDelete:CASCADE,OnUpdate:CASCADE"` CreatedAt time.Time UpdatedAt time.Time diff --git a/taco/internal/unit/handler.go b/taco/internal/unit/handler.go index 30cc15f93..28956c733 100644 --- a/taco/internal/unit/handler.go +++ b/taco/internal/unit/handler.go @@ -12,9 +12,8 @@ import ( "github.com/diggerhq/digger/opentaco/internal/deps" "github.com/diggerhq/digger/opentaco/internal/rbac" "github.com/diggerhq/digger/opentaco/internal/storage" - "github.com/diggerhq/digger/opentaco/internal/db" "github.com/diggerhq/digger/opentaco/internal/query" - "gorm.io/gorm" + "github.com/diggerhq/digger/opentaco/internal/query/types" "github.com/google/uuid" "github.com/labstack/echo/v4" "log" @@ -27,15 +26,15 @@ type Handler struct { store storage.UnitStore rbacManager *rbac.RBACManager signer *auth.Signer - db *gorm.DB + queryStore query.Store } -func NewHandler(store storage.UnitStore, rbacManager *rbac.RBACManager, signer *auth.Signer, db *gorm.DB) *Handler { +func NewHandler(store storage.UnitStore, rbacManager *rbac.RBACManager, signer *auth.Signer, queryStore query.Store) *Handler { return &Handler{ store: store, rbacManager: rbacManager, signer: signer, - db: db, + queryStore: queryStore, } } @@ -72,12 +71,12 @@ func (h *Handler) CreateUnit(c echo.Context) error { } // POC - write to db example - if h.db != nil { - if err := db.SyncCreateUnit(h.db, id); err != nil { - log.Printf("Warning: failed to sync unit creation to database: %v", err) - // Don't fail the request if DB sync fails - } - } + // if h.db != nil { + // if err := db.SyncCreateUnit(h.db, id); err != nil { + // log.Printf("Warning: failed to sync unit creation to database: %v", err) + // // Don't fail the request if DB sync fails + // } + // } analytics.SendEssential("unit_created") return c.JSON(http.StatusCreated, CreateUnitResponse{ID: metadata.ID, Created: metadata.Updated}) @@ -88,20 +87,18 @@ func (h *Handler) ListUnits(c echo.Context) error { prefix := c.QueryParam("prefix") - if unitQuery, ok := SupportsUnitQuery(h.queryStore); ok { - rbacQuery, hasRBAC := SupportsRBACQuery(h.queryStore) - - units, err := unitQuery.ListUnits(ctx, prefix) + if h.queryStore.IsEnabled() { + units, err := h.queryStore.ListUnits(ctx, prefix) if err == nil { // Index by ID unitIDs := make([]string, len(units)) - unitMap := make(map[string]query.Unit, len(units)) + unitMap := make(map[string]types.Unit, len(units)) for i, u := range units { unitIDs[i] = u.Name unitMap[u.Name] = u } - if hasRBAC && h.rbacManager != nil && h.signer != nil { + if h.rbacManager != nil && h.signer != nil { principal, perr := h.getPrincipalFromToken(c) if perr != nil { // If RBAC is enabled, return 401; otherwise skip RBAC @@ -111,7 +108,7 @@ func (h *Handler) ListUnits(c echo.Context) error { }) } } else { - filteredIDs, ferr := rbacQuery.FilterUnitIDsByUser(ctx, principal.Subject, unitIDs) + filteredIDs, ferr := h.queryStore.FilterUnitIDsByUser(ctx, principal.Subject, unitIDs) if ferr != nil { log.Printf("RBAC filtering via query-store failed; falling back to storage: %v", ferr) return h.listFromStorage(ctx, c, prefix) @@ -237,13 +234,13 @@ func (h *Handler) DeleteUnit(c echo.Context) error { return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to delete unit"}) } - // POC - write to db example - if h.db != nil { - if err := db.SyncDeleteUnit(h.db, id); err != nil { - log.Printf("Warning: failed to sync unit deletion to database: %v", err) - // Don't fail the request if DB sync fails - } - } + // // POC - write to db example + // if h.db != nil { + // if err := db.SyncDeleteUnit(h.db, id); err != nil { + // log.Printf("Warning: failed to sync unit deletion to database: %v", err) + // // Don't fail the request if DB sync fails + // } + // } return c.NoContent(http.StatusNoContent) @@ -296,11 +293,11 @@ func (h *Handler) UploadUnit(c echo.Context) error { return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to upload unit"}) } // POC - write to db - if h.db != nil { - if err := db.SyncUnitExists(h.db, id); err != nil { - log.Printf("Warning: failed to sync unit to database: %v", err) - } - } + // if h.db != nil { + // if err := db.SyncUnitExists(h.db, id); err != nil { + // log.Printf("Warning: failed to sync unit to database: %v", err) + // } + // } // Best-effort dependency graph update go deps.UpdateGraphOnWrite(c.Request().Context(), h.store, id, data) analytics.SendEssential("taco_unit_push_completed") @@ -457,62 +454,62 @@ func (h *Handler) GetUnitStatus(c echo.Context) error { // POC // Add this new method to the existing Handler struct -func (h *Handler) ListUnitsFast(c echo.Context) error { - prefix := c.QueryParam("prefix") +// func (h *Handler) ListUnitsFast(c echo.Context) error { +// prefix := c.QueryParam("prefix") - // 1. Get all units from DATABASE - allUnits, err := db.ListAllUnitsWithPrefix(h.db, prefix) - if err != nil { - return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to list units from database"}) - } - - // 2. Extract unit names and create map - unitNames := make([]string, 0, len(allUnits)) - unitMap := make(map[string]db.Unit) +// // 1. Get all units from DATABASE +// allUnits, err := db.ListAllUnitsWithPrefix(h.db, prefix) +// if err != nil { +// return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to list units from database"}) +// } + +// // 2. Extract unit names and create map +// unitNames := make([]string, 0, len(allUnits)) +// unitMap := make(map[string]db.Unit) - for _, unit := range allUnits { - unitNames = append(unitNames, unit.Name) - unitMap[unit.Name] = unit - } - - // 3. RBAC filter with DATABASE - if h.rbacManager != nil && h.signer != nil { - principal, err := h.getPrincipalFromToken(c) - if err != nil { - if enabled, _ := h.rbacManager.IsEnabled(c.Request().Context()); enabled { - return c.JSON(http.StatusUnauthorized, map[string]string{"error": "Failed to authenticate user"}) - } - } else { - // RBAC filtering - filteredNames, err := db.FilterUnitIDsByUser(h.db, principal.Subject, unitNames) - if err != nil { - return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to check permissions via database"}) - } - unitNames = filteredNames - } - } - - // 4. Build response - var responseUnits []*domain.Unit - for _, name := range unitNames { - if dbUnit, exists := unitMap[name]; exists { - // Convert db.Unit to domain.Unit - responseUnits = append(responseUnits, &domain.Unit{ - ID: dbUnit.Name, - Size: 0, // DB doesn't have size, could be calculated - Updated: time.Now(), // Could add timestamp to db.Unit - Locked: false, // Could check locks in database - }) - } - } +// for _, unit := range allUnits { +// unitNames = append(unitNames, unit.Name) +// unitMap[unit.Name] = unit +// } + +// // 3. RBAC filter with DATABASE +// if h.rbacManager != nil && h.signer != nil { +// principal, err := h.getPrincipalFromToken(c) +// if err != nil { +// if enabled, _ := h.rbacManager.IsEnabled(c.Request().Context()); enabled { +// return c.JSON(http.StatusUnauthorized, map[string]string{"error": "Failed to authenticate user"}) +// } +// } else { +// // RBAC filtering +// filteredNames, err := db.FilterUnitIDsByUser(h.db, principal.Subject, unitNames) +// if err != nil { +// return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to check permissions via database"}) +// } +// unitNames = filteredNames +// } +// } + +// // 4. Build response +// var responseUnits []*domain.Unit +// for _, name := range unitNames { +// if dbUnit, exists := unitMap[name]; exists { +// // Convert db.Unit to domain.Unit +// responseUnits = append(responseUnits, &domain.Unit{ +// ID: dbUnit.Name, +// Size: 0, // DB doesn't have size, could be calculated +// Updated: time.Now(), // Could add timestamp to db.Unit +// Locked: false, // Could check locks in database +// }) +// } +// } - domain.SortUnitsByID(responseUnits) - return c.JSON(http.StatusOK, map[string]interface{}{ - "units": responseUnits, - "count": len(responseUnits), - "source": "database", // POC identifier - }) -} +// domain.SortUnitsByID(responseUnits) +// return c.JSON(http.StatusOK, map[string]interface{}{ +// "units": responseUnits, +// "count": len(responseUnits), +// "source": "database", // POC identifier +// }) +// } // Helpers From c7a07ce2ecc2f2dd2a718c4e577184422dee3223 Mon Sep 17 00:00:00 2001 From: Brian Reardon Date: Mon, 6 Oct 2025 19:42:42 -0700 Subject: [PATCH 06/13] add db types, modular bucket, auth --- go.mod | 17 +- go.sum | 194 +++++++++++ taco/cmd/statesman/go.mod | 14 + taco/cmd/statesman/go.sum | 198 +++++++++++ taco/cmd/statesman/main.go | 122 +++++-- taco/internal/api/routes.go | 14 +- taco/internal/middleware/auth.go | 44 +++ taco/internal/principal/principal.go | 9 + taco/internal/query/common/sql_store.go | 441 ++++++++++++++++++++++++ taco/internal/query/config.go | 38 +- taco/internal/query/factory.go | 34 -- taco/internal/query/interface.go | 10 + taco/internal/query/mssql/store.go | 29 ++ taco/internal/query/mysql/store.go | 29 ++ taco/internal/query/postgres/store.go | 29 +- taco/internal/query/sqlite/store.go | 364 +------------------ taco/internal/query/types/errors.go | 1 - taco/internal/query/types/models.go | 13 +- taco/internal/queryfactory/factory.go | 33 ++ taco/internal/rbac/s3store.go | 271 ++++++++------- taco/internal/storage/authorizer.go | 208 +++++++++++ taco/internal/storage/interface.go | 3 +- taco/internal/storage/orchestrator.go | 156 +++++++++ taco/internal/unit/handler.go | 120 ++----- taco/internal/wiring/rbac.go | 69 ++++ 25 files changed, 1828 insertions(+), 632 deletions(-) create mode 100644 taco/internal/principal/principal.go create mode 100644 taco/internal/query/common/sql_store.go delete mode 100644 taco/internal/query/factory.go create mode 100644 taco/internal/query/mssql/store.go create mode 100644 taco/internal/query/mysql/store.go create mode 100644 taco/internal/queryfactory/factory.go create mode 100644 taco/internal/storage/authorizer.go create mode 100644 taco/internal/storage/orchestrator.go create mode 100644 taco/internal/wiring/rbac.go diff --git a/go.mod b/go.mod index 0df54e17b..3c0f6635c 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,26 @@ module github.com/diggerhq/digger go 1.24.0 require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect + github.com/golang-sql/sqlexp v0.1.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.6.0 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/kelseyhightower/envconfig v1.4.0 // indirect github.com/mattn/go-sqlite3 v1.14.22 // indirect - golang.org/x/text v0.20.0 // indirect + github.com/microsoft/go-mssqldb v1.8.2 // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/text v0.21.0 // indirect + gorm.io/driver/mysql v1.6.0 // indirect + gorm.io/driver/postgres v1.6.0 // indirect gorm.io/driver/sqlite v1.6.0 // indirect + gorm.io/driver/sqlserver v1.6.1 // indirect gorm.io/gorm v1.31.0 // indirect ) diff --git a/go.sum b/go.sum index db53b015e..1596e40e1 100644 --- a/go.sum +++ b/go.sum @@ -1,15 +1,209 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= +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/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1/go.mod h1:uE9zaUfEQT/nbQjVi2IblCG9iaLtZsuYZ8ne+PuQ02M= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= +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/internal v1.5.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +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/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= +github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= 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.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +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/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 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.8.2 h1:236sewazvC8FvG6Dr3bszrVhMkAl4KYImryLkRMCd0I= +github.com/microsoft/go-mssqldb v1.8.2/go.mod h1:vp38dT33FGfVotRiTmDo3bFyaHq+p3LektQrjTULowo= +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/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +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= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +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.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +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.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +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.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/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.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/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.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +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.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +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.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +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.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= +gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= +gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= +gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= +gorm.io/driver/sqlserver v1.6.1 h1:XWISFsu2I2pqd1KJhhTZNJMx1jNQ+zVL/Q8ovDcUjtY= +gorm.io/driver/sqlserver v1.6.1/go.mod h1:VZeNn7hqX1aXoN5TPAFGWvxWG90xtA8erGn2gQmpc6U= gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY= diff --git a/taco/cmd/statesman/go.mod b/taco/cmd/statesman/go.mod index 77144c459..30a7eca15 100644 --- a/taco/cmd/statesman/go.mod +++ b/taco/cmd/statesman/go.mod @@ -4,10 +4,12 @@ go 1.24 require ( github.com/diggerhq/digger/opentaco/internal v0.0.0 + github.com/kelseyhightower/envconfig v1.4.0 github.com/labstack/echo/v4 v4.11.4 ) require ( + filippo.io/edwards25519 v1.1.0 // indirect github.com/aws/aws-sdk-go-v2 v1.38.1 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.0 // indirect github.com/aws/aws-sdk-go-v2/config v1.31.2 // indirect @@ -28,16 +30,24 @@ require ( github.com/aws/smithy-go v1.22.5 // indirect github.com/coreos/go-oidc/v3 v3.11.0 // indirect github.com/go-jose/go-jose/v4 v4.0.2 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect + github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/google/jsonapi v1.0.0 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.6.0 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/labstack/gommon v0.4.2 // indirect github.com/mattn/go-colorable v0.1.13 // 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.8.2 // indirect github.com/mr-tron/base58 v1.2.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/stretchr/testify v1.10.0 // indirect @@ -46,10 +56,14 @@ require ( golang.org/x/crypto v0.32.0 // indirect golang.org/x/net v0.34.0 // indirect golang.org/x/oauth2 v0.24.0 // indirect + golang.org/x/sync v0.10.0 // indirect golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.7.0 // indirect + gorm.io/driver/mysql v1.6.0 // indirect + gorm.io/driver/postgres v1.6.0 // indirect gorm.io/driver/sqlite v1.6.0 // indirect + gorm.io/driver/sqlserver v1.6.1 // indirect gorm.io/gorm v1.31.0 // indirect ) diff --git a/taco/cmd/statesman/go.sum b/taco/cmd/statesman/go.sum index 294c98935..4abccac69 100644 --- a/taco/cmd/statesman/go.sum +++ b/taco/cmd/statesman/go.sum @@ -1,3 +1,23 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= +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/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1/go.mod h1:uE9zaUfEQT/nbQjVi2IblCG9iaLtZsuYZ8ne+PuQ02M= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 h1:U2rTu3Ef+7w9FHKIAXM6ZyqF3UOWJZ12zIm8zECAFfg= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= +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/internal v1.5.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 h1:jBQA3cKT4L2rWMpgE7Yt3Hwh2aUj8KXjIGLxjHeYNNo= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/aws/aws-sdk-go-v2 v1.38.1 h1:j7sc33amE74Rz0M/PoCpsZQ6OunLqys/m5antM0J+Z8= github.com/aws/aws-sdk-go-v2 v1.38.1/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.0 h1:6GMWV6CNpA/6fbFHnoAjrv4+LGfyTqZz2LtCHnspgDg= @@ -36,24 +56,67 @@ github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw= github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI= github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/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/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonapi v1.0.0 h1:qIGgO5Smu3yJmSs+QlvhQnrscdZfFhiV6S8ryJAglqU= github.com/google/jsonapi v1.0.0/go.mod h1:YYHiRPJT8ARXGER8In9VuLv4qvLfDmA9ULQqptbLE4s= +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/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= +github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= 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.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= +github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +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/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/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8= github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= @@ -65,33 +128,168 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE 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.8.2 h1:236sewazvC8FvG6Dr3bszrVhMkAl4KYImryLkRMCd0I= +github.com/microsoft/go-mssqldb v1.8.2/go.mod h1:vp38dT33FGfVotRiTmDo3bFyaHq+p3LektQrjTULowo= +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/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +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= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +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/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +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.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +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.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +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.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/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.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +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-20220520151302-bc2c85ada10a/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.1.0/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.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.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/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +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.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +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.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +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.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +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.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= +gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= +gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= +gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= +gorm.io/driver/sqlserver v1.6.1 h1:XWISFsu2I2pqd1KJhhTZNJMx1jNQ+zVL/Q8ovDcUjtY= +gorm.io/driver/sqlserver v1.6.1/go.mod h1:VZeNn7hqX1aXoN5TPAFGWvxWG90xtA8erGn2gQmpc6U= +gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY= gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= diff --git a/taco/cmd/statesman/main.go b/taco/cmd/statesman/main.go index 762a11958..d3ca93425 100644 --- a/taco/cmd/statesman/main.go +++ b/taco/cmd/statesman/main.go @@ -21,17 +21,21 @@ import ( "github.com/diggerhq/digger/opentaco/internal/analytics" "github.com/diggerhq/digger/opentaco/internal/api" + "github.com/diggerhq/digger/opentaco/internal/auth" + "github.com/diggerhq/digger/opentaco/internal/middleware" "github.com/diggerhq/digger/opentaco/internal/query" + "github.com/diggerhq/digger/opentaco/internal/queryfactory" "github.com/diggerhq/digger/opentaco/internal/storage" + "github.com/diggerhq/digger/opentaco/internal/wiring" "github.com/kelseyhightower/envconfig" "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" + echomiddleware "github.com/labstack/echo/v4/middleware" ) func main() { var ( port = flag.String("port", "8080", "Server port") - authDisable = flag.Bool("auth-disable", false, "Disable auth enforcement (default: false)") + authDisable = flag.Bool("auth-disable", os.Getenv("OPENTACO_AUTH_DISABLE") == "true", "Disable auth enforcement (default: false)") storageType = flag.String("storage", "s3", "Storage type: s3 or memory (default: s3 with fallback to memory)") s3Bucket = flag.String("s3-bucket", os.Getenv("OPENTACO_S3_BUCKET"), "S3 bucket for state storage") s3Prefix = flag.String("s3-prefix", os.Getenv("OPENTACO_S3_PREFIX"), "S3 key prefix (optional)") @@ -46,14 +50,17 @@ func main() { log.Fatalf("Failed to process configuration: %v", err) } - // Pass the populated config struct to the factory. - queryStore, err := query.NewQueryStore(queryCfg) + // --- Initialize Stores --- + + // Create the database index store using the dedicated factory. + queryStore, err := queryfactory.NewQueryStore(queryCfg) if err != nil { log.Fatalf("Failed to initialize query backend: %v", err) } - defer queryStore.Close() + log.Printf("Query backend initialized: %s (enabled: %v)", queryCfg.Backend, queryStore.IsEnabled()) + if queryStore.IsEnabled(){ log.Println("Query backend enabled successfully") }else{ @@ -62,40 +69,90 @@ func main() { // Initialize storage - var store storage.UnitStore + var blobStore storage.UnitStore switch *storageType { case "s3": if *s3Bucket == "" { log.Printf("WARNING: S3 storage selected but bucket not provided. Falling back to in-memory storage.") - store = storage.NewMemStore() + blobStore = storage.NewMemStore() log.Printf("Using in-memory storage") break } s, err := storage.NewS3Store(context.Background(), *s3Bucket, *s3Prefix, *s3Region) if err != nil { log.Printf("WARNING: failed to initialize S3 store: %v. Falling back to in-memory storage.", err) - store = storage.NewMemStore() + blobStore = storage.NewMemStore() log.Printf("Using in-memory storage") } else { - store = s + blobStore = s log.Printf("Using S3 storage: bucket=%s prefix=%s region=%s", *s3Bucket, *s3Prefix, *s3Region) - - //put on thread thread / adjust seed so it accepts any store - // To this: - // if s3Store, ok := store.(storage.S3Store); ok { - // db.Seed(context.Background(), s3Store, database) - // } else { - // log.Println("Store is not S3Store, skipping seeding") - // } } default: - store = storage.NewMemStore() + blobStore = storage.NewMemStore() log.Printf("Using in-memory storage") } + + // 3. Create the base OrchestratingStore + orchestratingStore := storage.NewOrchestratingStore(blobStore, queryStore) + + // --- Sync RBAC Data --- + if queryStore.IsEnabled() { + if err := wiring.SyncRBACFromStorage(context.Background(), blobStore, queryStore); err != nil { + log.Printf("Warning: Failed to sync RBAC data: %v", err) + } + + // Sync existing units from storage to database + log.Println("Syncing existing units from storage to database...") + units, err := blobStore.List(context.Background(), "") + if err != nil { + log.Printf("Warning: Failed to list units from storage: %v", err) + } else { + log.Printf("DEBUG: Got %d units from storage", len(units)) + for _, unit := range units { + log.Printf("DEBUG: Unit from storage: ID=%s, Size=%d, Updated=%v", unit.ID, unit.Size, unit.Updated) + + // Always ensure unit exists first + if err := queryStore.SyncEnsureUnit(context.Background(), unit.ID); err != nil { + log.Printf("Warning: Failed to sync unit %s: %v", unit.ID, err) + continue + } + + // Always sync metadata to update existing records + log.Printf("Syncing metadata for %s: size=%d, updated=%v", unit.ID, unit.Size, unit.Updated) + if err := queryStore.SyncUnitMetadata(context.Background(), unit.ID, unit.Size, unit.Updated); err != nil { + log.Printf("Warning: Failed to sync metadata for unit %s: %v", unit.ID, err) + } + } + log.Printf("Synced %d units from storage to database", len(units)) + } + } + + // --- Conditionally Apply Authorization Layer with a SMART CHECK --- + var finalStore storage.UnitStore + + // Check if there are any RBAC roles defined in the database. + rbacIsConfigured, err := queryStore.HasRBACRoles(context.Background()) + if err != nil { + log.Fatalf("Failed to check for RBAC configuration: %v", err) + } + + // The condition is now two-part: Auth must be enabled AND RBAC roles must exist. + if !*authDisable && rbacIsConfigured { + log.Println("RBAC is ENABLED and CONFIGURED. Wrapping store with authorization layer.") + finalStore = storage.NewAuthorizingStore(orchestratingStore, queryStore) + } else { + if !*authDisable { + log.Println("RBAC is ENABLED but NOT CONFIGURED (no roles found). Authorization layer will be skipped.") + } else { + log.Println("RBAC is DISABLED via flag. Authorization layer will be skipped.") + } + finalStore = orchestratingStore + } + // Initialize analytics with system ID management (always create system ID) - analytics.InitGlobalWithSystemID("production", store) + analytics.InitGlobalWithSystemID("production", finalStore) // Initialize system ID synchronously during startup ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) @@ -119,14 +176,26 @@ func main() { e.HideBanner = true // Middleware - e.Use(middleware.Logger()) - e.Use(middleware.Recover()) - e.Use(middleware.RequestID()) - e.Use(middleware.Gzip()) - e.Use(middleware.Secure()) - e.Use(middleware.CORS()) + e.Use(echomiddleware.Logger()) + e.Use(echomiddleware.Recover()) + e.Use(echomiddleware.RequestID()) + e.Use(echomiddleware.Gzip()) + e.Use(echomiddleware.Secure()) + e.Use(echomiddleware.CORS()) - api.RegisterRoutes(e, store, !*authDisable, queryStore) + + signer, err := auth.NewSignerFromEnv() + if err != nil { + log.Fatalf("Failed to initialize JWT signer: %v", err) + } + + // Conditionally apply the authentication middleware. + if !*authDisable { + e.Use(middleware.JWTAuthMiddleware(signer)) + } + + // Pass the same signer instance to routes + api.RegisterRoutes(e, finalStore, !*authDisable, queryStore, blobStore, signer) // Start server go func() { @@ -157,3 +226,4 @@ func main() { analytics.SendEssential("server_shutdown_complete") log.Println("Server shutdown complete") } + diff --git a/taco/internal/api/routes.go b/taco/internal/api/routes.go index 5892f8ff6..5fa848a81 100644 --- a/taco/internal/api/routes.go +++ b/taco/internal/api/routes.go @@ -25,7 +25,7 @@ import ( ) // RegisterRoutes registers all API routes -func RegisterRoutes(e *echo.Echo, store storage.UnitStore, authEnabled bool, queryStore query.QueryStore) { +func RegisterRoutes(e *echo.Echo, store storage.UnitStore, authEnabled bool, queryStore query.Store, underlyingStore storage.UnitStore, signer *authpkg.Signer) { // Health checks health := observability.NewHealthHandler() e.GET("/healthz", health.Healthz) @@ -52,11 +52,7 @@ func RegisterRoutes(e *echo.Echo, store storage.UnitStore, authEnabled bool, que }) - // Prepare auth deps - signer, err := authpkg.NewSignerFromEnv() - if err != nil { - fmt.Printf("Failed to create JWT signer: %v\n", err) - } + // Prepare auth deps stsi, _ := sts.NewStatelessIssuerFromEnv() ver, _ := oidc.NewFromEnv() @@ -137,10 +133,10 @@ func RegisterRoutes(e *echo.Echo, store storage.UnitStore, authEnabled bool, que v1.Use(middleware.RequireAuth(verifyFn)) } - // Setup RBAC manager if available + // Setup RBAC manager if available (use underlyingStore for type assertion) var rbacManager *rbac.RBACManager - if store != nil { - if s3Store, ok := store.(storage.S3Store); ok { + if underlyingStore != nil { + if s3Store, ok := underlyingStore.(storage.S3Store); ok { rbacStore := rbac.NewS3RBACStore(s3Store.GetS3Client(), s3Store.GetS3Bucket(), s3Store.GetS3Prefix()) rbacManager = rbac.NewRBACManager(rbacStore) } diff --git a/taco/internal/middleware/auth.go b/taco/internal/middleware/auth.go index bb958553d..92a11f948 100644 --- a/taco/internal/middleware/auth.go +++ b/taco/internal/middleware/auth.go @@ -3,10 +3,13 @@ package middleware import ( "net/http" "strings" + "log" "github.com/diggerhq/digger/opentaco/internal/auth" "github.com/diggerhq/digger/opentaco/internal/rbac" + "github.com/diggerhq/digger/opentaco/internal/storage" "github.com/labstack/echo/v4" + "github.com/diggerhq/digger/opentaco/internal/principal" ) // AccessTokenVerifier is a function that validates an access token. @@ -67,6 +70,47 @@ func RBACMiddleware(rbacManager *rbac.RBACManager, signer *auth.Signer, action r } } +// JWTAuthMiddleware creates a middleware that verifies a JWT and injects the user principal into the request context. +func JWTAuthMiddleware(signer *auth.Signer) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + log.Printf("DEBUG JWTAuthMiddleware: Called for path: %s", c.Request().URL.Path) + + authz := c.Request().Header.Get("Authorization") + if !strings.HasPrefix(authz, "Bearer ") { + log.Printf("DEBUG JWTAuthMiddleware: No Bearer token found") + // No token, continue. The AuthorizingStore will block the request. + return next(c) + } + + token := strings.TrimSpace(strings.TrimPrefix(authz, "Bearer ")) + claims, err := signer.VerifyAccess(token) + if err != nil { + log.Printf("DEBUG JWTAuthMiddleware: Token verification failed: %v", err) + // Invalid token, continue. The AuthorizingStore will block the request. + return next(c) + } + + log.Printf("DEBUG JWTAuthMiddleware: Token verified for subject: %s", claims.Subject) + + p := principal.Principal{ + Subject: claims.Subject, + Email: claims.Email, + Roles: claims.Roles, + Groups: claims.Groups, + } + + // Add the principal to the context for downstream stores and handlers. + ctx := storage.ContextWithPrincipal(c.Request().Context(), p) + c.SetRequest(c.Request().WithContext(ctx)) + + log.Printf("DEBUG JWTAuthMiddleware: Principal set in context for subject: %s", claims.Subject) + + return next(c) + } + } +} + // getPrincipalFromToken extracts principal information from the bearer token func getPrincipalFromToken(c echo.Context, signer *auth.Signer) (rbac.Principal, error) { authz := c.Request().Header.Get("Authorization") diff --git a/taco/internal/principal/principal.go b/taco/internal/principal/principal.go new file mode 100644 index 000000000..d4524fa39 --- /dev/null +++ b/taco/internal/principal/principal.go @@ -0,0 +1,9 @@ +package principal + +// Principal represents the identity of an authenticated user or service. +type Principal struct { + Subject string + Email string + Roles []string + Groups []string +} diff --git a/taco/internal/query/common/sql_store.go b/taco/internal/query/common/sql_store.go new file mode 100644 index 000000000..1ad7b5306 --- /dev/null +++ b/taco/internal/query/common/sql_store.go @@ -0,0 +1,441 @@ +package common + +import ( + "context" + "errors" + "fmt" + "log" + "strings" + "time" + + "github.com/diggerhq/digger/opentaco/internal/query/types" + "github.com/diggerhq/digger/opentaco/internal/rbac" + "gorm.io/gorm" +) + +// SQLStore provides a generic, GORM-based implementation of the Store interface. +// It can be used with any GORM-compatible database dialect (SQLite, Postgres, etc.). +type SQLStore struct { + db *gorm.DB +} + +// NewSQLStore is a constructor for our common store. It takes a pre-configured +// GORM DB object and handles the common setup tasks like migration and view creation. +func NewSQLStore(db *gorm.DB) (*SQLStore, error) { + store := &SQLStore{db: db} + + if err := store.migrate(); err != nil { + return nil, fmt.Errorf("failed to migrate common sql schema: %w", err) + } + if err := store.createViews(); err != nil { + return nil, fmt.Errorf("failed to create common sql views: %w", err) + } + + return store, nil +} + +func (s *SQLStore) migrate() error { + return s.db.AutoMigrate(types.DefaultModels...) +} + +// createViews now introspects the database dialect to use the correct SQL syntax. +func (s *SQLStore) createViews() error { + // Define the body of the view once. + viewBody := ` + WITH user_permissions AS ( + SELECT DISTINCT u.subject as user_subject, r.id as rule_id, r.wildcard_resource, r.effect FROM users u + JOIN user_roles ur ON u.id = ur.user_id JOIN role_permissions rp ON ur.role_id = rp.role_id + JOIN rules r ON rp.permission_id = r.permission_id LEFT JOIN rule_actions ra ON r.id = ra.rule_id + WHERE r.effect = 'allow' AND (r.wildcard_action = true OR ra.action = 'unit.read' OR ra.action IS NULL) + ), + wildcard_access AS ( + SELECT DISTINCT up.user_subject, un.name as unit_name FROM user_permissions up CROSS JOIN units un + WHERE up.wildcard_resource = true + ), + specific_access AS ( + SELECT DISTINCT up.user_subject, un.name as unit_name FROM user_permissions up + JOIN rule_units ru ON up.rule_id = ru.rule_id JOIN units un ON ru.unit_id = un.id + WHERE up.wildcard_resource = false + ) + SELECT user_subject, unit_name FROM wildcard_access + UNION + SELECT user_subject, unit_name FROM specific_access + ` + + var createViewSQL string + dialect := s.db.Dialector.Name() + + // This switch statement is our "carve-out" for different SQL dialects. + switch dialect { + case "sqlserver": + createViewSQL = fmt.Sprintf("CREATE OR ALTER VIEW user_unit_access AS %s", viewBody) + case "sqlite", "postgres": + fallthrough // Use the same syntax for both + default: + // Default to the most common syntax. + createViewSQL = fmt.Sprintf("CREATE OR REPLACE VIEW user_unit_access AS %s", viewBody) + } + + return s.db.Exec(createViewSQL).Error +} + +func (s *SQLStore) Close() error { + sqlDB, err := s.db.DB() + if err != nil { + return err + } + return sqlDB.Close() +} + +func (s *SQLStore) IsEnabled() bool { return true } + +func (s *SQLStore) ListUnits(ctx context.Context, prefix string) ([]types.Unit, error) { + var units []types.Unit + q := s.db.WithContext(ctx).Preload("Tags") + if prefix != "" { + q = q.Where("name LIKE ?", prefix+"%") + } + return units, q.Find(&units).Error +} + +func (s *SQLStore) GetUnit(ctx context.Context, id string) (*types.Unit, error) { + var unit types.Unit + err := s.db.WithContext(ctx).Preload("Tags").Where("name = ?", id).First(&unit).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, types.ErrNotFound + } + return nil, err + } + return &unit, nil +} + +func (s *SQLStore) SyncEnsureUnit(ctx context.Context, unitName string) error { + unit := types.Unit{Name: unitName} + return s.db.WithContext(ctx).FirstOrCreate(&unit, types.Unit{Name: unitName}).Error +} + + +func (s *SQLStore) SyncUnitMetadata(ctx context.Context, unitName string, size int64, updated time.Time) error { + return s.db.WithContext(ctx).Model(&types.Unit{}). + Where("name = ?", unitName). + Updates(map[string]interface{}{ + "size": size, + "updated_at": updated, + }).Error +} + +func (s *SQLStore) SyncDeleteUnit(ctx context.Context, unitName string) error { + return s.db.WithContext(ctx).Where("name = ?", unitName).Delete(&types.Unit{}).Error +} + +func (s *SQLStore) SyncUnitLock(ctx context.Context, unitName string, lockID, lockWho string, lockCreated time.Time) error { + return s.db.WithContext(ctx).Model(&types.Unit{}). + Where("name = ?", unitName). + Updates(map[string]interface{}{ + "locked": true, + "lock_id": lockID, + "lock_who": lockWho, + "lock_created": lockCreated, + }).Error +} + +func (s *SQLStore) SyncUnitUnlock(ctx context.Context, unitName string) error { + return s.db.WithContext(ctx).Model(&types.Unit{}). + Where("name = ?", unitName). + Updates(map[string]interface{}{ + "locked": false, + "lock_id": "", + "lock_who": "", + "lock_created": time.Time{}, + }).Error +} + +func (s *SQLStore) ListUnitsForUser(ctx context.Context, userSubject string, prefix string) ([]types.Unit, error) { + var units []types.Unit + q := s.db.WithContext(ctx).Table("units").Select("units.*"). + Joins("JOIN user_unit_access ON units.name = user_unit_access.unit_name"). + Where("user_unit_access.user_subject = ?", userSubject). + Preload("Tags") + + if prefix != "" { + q = q.Where("units.name LIKE ?", prefix+"%") + } + + // DEBUG: Let's see what's being queried + log.Printf("DEBUG ListUnitsForUser: userSubject=%s, prefix=%s", userSubject, prefix) + + err := q.Find(&units).Error + + log.Printf("DEBUG ListUnitsForUser: found %d units, error: %v", len(units), err) + + return units, err +} + +func (s *SQLStore) FilterUnitIDsByUser(ctx context.Context, userSubject string, unitIDs []string) ([]string, error) { + if len(unitIDs) == 0 { + return []string{}, nil + } + var allowedUnitIDs []string + return allowedUnitIDs, s.db.WithContext(ctx).Table("user_unit_access"). + Select("unit_name"). + Where("user_subject = ?", userSubject). + Where("unit_name IN ?", unitIDs). + Pluck("unit_name", &allowedUnitIDs).Error +} + +func (s *SQLStore) CanPerformAction(ctx context.Context, userSubject string, action string, resourceID string) (bool, error) { + var allowed int + // GORM's Raw SQL uses '?' and the dialect converts it to '$1', etc. for Postgres automatically. + querySQL := ` + SELECT MAX(CASE WHEN r.effect = 'allow' THEN 1 ELSE 0 END) FROM users u + JOIN user_roles ur ON u.id = ur.user_id JOIN role_permissions rp ON ur.role_id = rp.role_id + JOIN rules r ON rp.permission_id = r.permission_id + WHERE u.subject = ? AND (r.wildcard_action = true OR EXISTS (SELECT 1 FROM rule_actions ra WHERE ra.rule_id = r.id AND ra.action = ?)) + AND (r.wildcard_resource = true OR EXISTS (SELECT 1 FROM rule_units ru JOIN units un ON ru.unit_id = un.id WHERE ru.rule_id = r.id AND un.name = ?)) + ` + err := s.db.WithContext(ctx).Raw(querySQL, userSubject, action, resourceID).Scan(&allowed).Error + return allowed == 1, err +} + +func (s *SQLStore) HasRBACRoles(ctx context.Context) (bool, error) { + var count int64 + // We don't need to count them all, we just need to know if at least one exists. + if err := s.db.WithContext(ctx).Model(&types.Role{}).Limit(1).Count(&count).Error; err != nil { + return false, err + } + return count > 0, nil +} + +// SyncPermission syncs a permission from storage to the database +func (s *SQLStore) SyncPermission(ctx context.Context, permissionData interface{}) error { + // Import at the top: "github.com/diggerhq/digger/opentaco/internal/rbac" + perm, ok := permissionData.(*rbac.Permission) + if !ok { + return fmt.Errorf("invalid permission data type") + } + + return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + // 1) Upsert the permission + p := types.Permission{ + PermissionId: perm.ID, + Name: perm.Name, + Description: perm.Description, + CreatedBy: perm.CreatedBy, + CreatedAt: perm.CreatedAt, + } + + // Upsert using FirstOrCreate + if err := tx.Where(types.Permission{PermissionId: perm.ID}). + Assign(p). + FirstOrCreate(&p).Error; err != nil { + return fmt.Errorf("upsert permission %s: %w", perm.ID, err) + } + + // 2) Clear old rules for idempotency + if err := tx.Where("permission_id = ?", p.ID).Delete(&types.Rule{}).Error; err != nil { + return fmt.Errorf("clear rules for %s: %w", perm.ID, err) + } + + // 3) Insert new rules + for _, ruleData := range perm.Rules { + rule := types.Rule{ + PermissionID: p.ID, + Effect: strings.ToLower(ruleData.Effect), + WildcardAction: hasStarAction(ruleData.Actions), + WildcardResource: hasStarResource(ruleData.Resources), + } + + if err := tx.Create(&rule).Error; err != nil { + return fmt.Errorf("create rule: %w", err) + } + + // Create rule actions if not wildcard + if !rule.WildcardAction { + for _, action := range ruleData.Actions { + ra := types.RuleAction{ + RuleID: rule.ID, + Action: string(action), + } + if err := tx.Create(&ra).Error; err != nil { + return fmt.Errorf("create rule action: %w", err) + } + } + } + + // Create rule units if not wildcard + if !rule.WildcardResource { + for _, resourceName := range ruleData.Resources { + // Ensure unit exists + var unit types.Unit + if err := tx.Where("name = ?", resourceName). + Attrs(types.Unit{Name: resourceName}). + FirstOrCreate(&unit).Error; err != nil { + return fmt.Errorf("ensure unit %q: %w", resourceName, err) + } + + ru := types.RuleUnit{ + RuleID: rule.ID, + UnitID: unit.ID, + } + if err := tx.Create(&ru).Error; err != nil { + return fmt.Errorf("create rule unit: %w", err) + } + } + } + } + + return nil + }) +} + +// SyncRole syncs a role from storage to the database +func (s *SQLStore) SyncRole(ctx context.Context, roleData interface{}) error { + role, ok := roleData.(*rbac.Role) + if !ok { + return fmt.Errorf("invalid role data type") + } + + return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + // 1) Upsert role + r := types.Role{ + RoleId: role.ID, + Name: role.Name, + Description: role.Description, + CreatedBy: role.CreatedBy, + CreatedAt: role.CreatedAt, + } + + if err := tx.Where(types.Role{RoleId: role.ID}). + Assign(r). + FirstOrCreate(&r).Error; err != nil { + return fmt.Errorf("upsert role %q: %w", role.ID, err) + } + + // 2) Find all referenced permissions + perms := make([]types.Permission, 0, len(role.Permissions)) + if len(role.Permissions) > 0 { + var existing []types.Permission + if err := tx.Where("permission_id IN ?", role.Permissions).Find(&existing).Error; err != nil { + return fmt.Errorf("lookup permissions for role %q: %w", role.ID, err) + } + + exists := make(map[string]types.Permission) + for _, p := range existing { + exists[p.PermissionId] = p + } + + // Create missing permissions as placeholders + for _, pid := range role.Permissions { + if p, ok := exists[pid]; ok { + perms = append(perms, p) + } else { + np := types.Permission{ + PermissionId: pid, + Name: pid, + Description: "", + CreatedBy: role.CreatedBy, + } + if err := tx.Where(types.Permission{PermissionId: pid}). + Attrs(np). + FirstOrCreate(&np).Error; err != nil { + return fmt.Errorf("create missing permission %q: %w", pid, err) + } + perms = append(perms, np) + } + } + } + + // 3) Replace role->permission associations + if err := tx.Model(&r).Association("Permissions").Replace(perms); err != nil { + return fmt.Errorf("set role permissions for %q: %w", role.ID, err) + } + + return nil + }) +} + +// SyncUser syncs a user assignment from storage to the database +func (s *SQLStore) SyncUser(ctx context.Context, userData interface{}) error { + user, ok := userData.(*rbac.UserAssignment) + if !ok { + return fmt.Errorf("invalid user data type") + } + + return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + // 1) Upsert user + u := types.User{ + Subject: user.Subject, + Email: user.Email, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, + Version: user.Version, + } + + if err := tx.Where(types.User{Subject: user.Subject}). + Assign(u). + FirstOrCreate(&u).Error; err != nil { + return fmt.Errorf("upsert user %q: %w", user.Subject, err) + } + + // 2) Find all referenced roles + roles := make([]types.Role, 0, len(user.Roles)) + if len(user.Roles) > 0 { + var existing []types.Role + if err := tx.Where("role_id IN ?", user.Roles).Find(&existing).Error; err != nil { + return fmt.Errorf("lookup roles: %w", err) + } + + byID := make(map[string]types.Role) + for _, r := range existing { + byID[r.RoleId] = r + } + + // Create missing roles as placeholders + for _, rid := range user.Roles { + if r, ok := byID[rid]; ok { + roles = append(roles, r) + } else { + nr := types.Role{ + RoleId: rid, + Name: rid, + Description: "", + CreatedBy: user.Subject, + } + if err := tx.Where(types.Role{RoleId: rid}). + Attrs(nr). + FirstOrCreate(&nr).Error; err != nil { + return fmt.Errorf("create missing role %q: %w", rid, err) + } + roles = append(roles, nr) + } + } + } + + // 3) Replace user->role associations + if err := tx.Model(&u).Association("Roles").Replace(roles); err != nil { + return fmt.Errorf("set user roles for %q: %w", user.Subject, err) + } + + return nil + }) +} + +// Helper functions for checking wildcards +func hasStarAction(actions []rbac.Action) bool { + for _, a := range actions { + if string(a) == "*" { + return true + } + } + return false +} + +func hasStarResource(resources []string) bool { + for _, r := range resources { + if r == "*" { + return true + } + } + return false +} diff --git a/taco/internal/query/config.go b/taco/internal/query/config.go index 6f9602485..19f2348c1 100644 --- a/taco/internal/query/config.go +++ b/taco/internal/query/config.go @@ -2,14 +2,16 @@ package query import "time" -// Config holds all configuration for the query store, loaded from environment variables. + type Config struct { - Backend string `envconfig:"QUERY_BACKEND" default:"sqlite"` - SQLite SQLiteConfig `envconfig:"SQLITE"` - // Postgres PostgresConfig `envconfig:"POSTGRES"` + Backend string `envconfig:"QUERY_BACKEND" default:"sqlite"` + SQLite SQLiteConfig `envconfig:"SQLITE"` + Postgres PostgresConfig `envconfig:"POSTGRES"` + MSSQL MSSQLConfig `envconfig:"MSSQL"` + MySQL MySQLConfig `envconfig:"MYSQL"` } -// SQLiteConfig holds all the specific settings for the SQLite backend. + type SQLiteConfig struct { Path string `envconfig:"PATH" default:"./data/taco.db"` Cache string `envconfig:"CACHE" default:"shared"` @@ -19,4 +21,30 @@ type SQLiteConfig struct { PragmaJournalMode string `envconfig:"PRAGMA_JOURNAL_MODE" default:"WAL"` PragmaForeignKeys string `envconfig:"PRAGMA_FOREIGN_KEYS" default:"ON"` PragmaBusyTimeout string `envconfig:"PRAGMA_BUSY_TIMEOUT" default:"5000"` +} + +type PostgresConfig struct { + Host string `envconfig:"HOST" default:"localhost"` + Port int `envconfig:"PORT" default:"5432"` + User string `envconfig:"USER" default:"postgres"` + Password string `envconfig:"PASSWORD"` + DBName string `envconfig:"DBNAME" default:"taco"` + SSLMode string `envconfig:"SSLMODE" default:"disable"` +} + +type MSSQLConfig struct { + Host string `envconfig:"HOST" default:"localhost"` + Port int `envconfig:"PORT" default:"1433"` + User string `envconfig:"USER"` + Password string `envconfig:"PASSWORD"` + DBName string `envconfig:"DBNAME" default:"taco"` +} + +type MySQLConfig struct { + Host string `envconfig:"HOST" default:"localhost"` + Port int `envconfig:"PORT" default:"3306"` + User string `envconfig:"USER" default:"root"` + Password string `envconfig:"PASSWORD"` + DBName string `envconfig:"DBNAME" default:"taco"` + Charset string `envconfig:"CHARSET" default:"utf8mb4"` } \ No newline at end of file diff --git a/taco/internal/query/factory.go b/taco/internal/query/factory.go deleted file mode 100644 index 7b34fed00..000000000 --- a/taco/internal/query/factory.go +++ /dev/null @@ -1,34 +0,0 @@ -package query - -import ( - "fmt" - "strings" - - "github.com/diggerhq/digger/opentaco/internal/query/noop" - "github.com/diggerhq/digger/opentaco/internal/query/sqlite" -) - -// NewQueryStore creates a new query.Store based on the provided configuration. -func NewQueryStore(cfg Config) (Store, error) { - backend := strings.ToLower(cfg.Backend) - - switch backend { - case "sqlite", "": - // Map our config struct to the one sqlite's New function expects. - sqliteCfg := sqlite.Config{ - Path: cfg.SQLite.Path, - Cache: cfg.SQLite.Cache, - BusyTimeout: cfg.SQLite.BusyTimeout, - MaxOpenConns: cfg.SQLite.MaxOpenConns, - MaxIdleConns: cfg.SQLite.MaxIdleConns, - PragmaJournalMode: cfg.SQLite.PragmaJournalMode, - PragmaForeignKeys: cfg.SQLite.PragmaForeignKeys, - PragmaBusyTimeout: cfg.SQLite.PragmaBusyTimeout, - } - return sqlite.NewSQLiteQueryStore(sqliteCfg) - case "off": - return noop.NewNoOpQueryStore(), nil - default: - return nil, fmt.Errorf("unsupported TACO_QUERY_BACKEND value: %q", backend) - } -} \ No newline at end of file diff --git a/taco/internal/query/interface.go b/taco/internal/query/interface.go index 6188e4a98..adf407100 100644 --- a/taco/internal/query/interface.go +++ b/taco/internal/query/interface.go @@ -3,6 +3,7 @@ package query import ( "context" "github.com/diggerhq/digger/opentaco/internal/query/types" + "time" ) type QueryStore interface { @@ -14,12 +15,21 @@ type UnitQuery interface { ListUnits(ctx context.Context, prefix string) ([]types.Unit, error) GetUnit(ctx context.Context, id string) (*types.Unit, error) SyncEnsureUnit(ctx context.Context, unitName string) error + SyncUnitMetadata(ctx context.Context, unitName string, size int64, updated time.Time) error + SyncUnitLock(ctx context.Context, unitName string, lockID, lockWho string, lockCreated time.Time) error + SyncUnitUnlock(ctx context.Context, unitName string) error SyncDeleteUnit(ctx context.Context, unitName string) error } type RBACQuery interface { FilterUnitIDsByUser(ctx context.Context, userSubject string, unitIDs []string) ([]string, error) ListUnitsForUser(ctx context.Context, userSubject string, prefix string) ([]types.Unit, error) + CanPerformAction(ctx context.Context, userSubject string, action string, resourceID string) (bool, error) + HasRBACRoles(ctx context.Context) (bool, error) + + SyncPermission(ctx context.Context, permission interface{}) error + SyncRole(ctx context.Context, role interface{}) error + SyncUser(ctx context.Context, user interface{}) error } type Store interface { diff --git a/taco/internal/query/mssql/store.go b/taco/internal/query/mssql/store.go new file mode 100644 index 000000000..348514cc4 --- /dev/null +++ b/taco/internal/query/mssql/store.go @@ -0,0 +1,29 @@ +package mssql + +import ( + "fmt" + + "github.com/diggerhq/digger/opentaco/internal/query" + "github.com/diggerhq/digger/opentaco/internal/query/common" + "gorm.io/driver/sqlserver" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +// NewMSSQLStore creates a new MS SQL-backed query store. +// Its only job is to establish the DB connection and pass it to the common SQLStore. +func NewMSSQLStore(cfg query.MSSQLConfig) (query.Store, error) { + // DSN format: sqlserver://username:password@host:port?database=dbname + dsn := fmt.Sprintf("sqlserver://%s:%s@%s:%d?database=%s", + cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.DBName) + + db, err := gorm.Open(sqlserver.Open(dsn), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Info), // Or Silent + }) + if err != nil { + return nil, fmt.Errorf("failed to connect to mssql: %w", err) + } + + // Hand off to the common, dialect-aware SQLStore engine. + return common.NewSQLStore(db) +} diff --git a/taco/internal/query/mysql/store.go b/taco/internal/query/mysql/store.go new file mode 100644 index 000000000..97c83b16e --- /dev/null +++ b/taco/internal/query/mysql/store.go @@ -0,0 +1,29 @@ +package mysql + +import ( + "fmt" + + "github.com/diggerhq/digger/opentaco/internal/query" + "github.com/diggerhq/digger/opentaco/internal/query/common" + "gorm.io/driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +// NewMySQLStore creates a new MySQL-backed query store. +// Its only job is to establish the DB connection and pass it to the common SQLStore. +func NewMySQLStore(cfg query.MySQLConfig) (query.Store, error) { + // DSN format: user:pass@tcp(host:port)/dbname?charset=utf8mb4&parseTime=True&loc=Local + dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=True&loc=Local", + cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.DBName, cfg.Charset) + + db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Info), // Or Silent + }) + if err != nil { + return nil, fmt.Errorf("failed to connect to mysql: %w", err) + } + + // Hand off to the common, dialect-aware SQLStore engine. + return common.NewSQLStore(db) +} diff --git a/taco/internal/query/postgres/store.go b/taco/internal/query/postgres/store.go index 233b9ca00..bf7fe9ba2 100644 --- a/taco/internal/query/postgres/store.go +++ b/taco/internal/query/postgres/store.go @@ -1 +1,28 @@ -package postgres \ No newline at end of file +package postgres + +import ( + "fmt" + + "github.com/diggerhq/digger/opentaco/internal/query" + "github.com/diggerhq/digger/opentaco/internal/query/common" + "gorm.io/driver/postgres" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +// NewPostgresStore creates a new PostgreSQL-backed query store. +// Its only job is to establish the DB connection and pass it to the common SQLStore. +func NewPostgresStore(cfg query.PostgresConfig) (query.Store, error) { + dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=%s", + cfg.Host, cfg.User, cfg.Password, cfg.DBName, cfg.Port, cfg.SSLMode) + + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Info), // Or Silent + }) + if err != nil { + return nil, fmt.Errorf("failed to connect to postgres: %w", err) + } + + // Call the constructor from the 'common' package, breaking the cycle. + return common.NewSQLStore(db) +} \ No newline at end of file diff --git a/taco/internal/query/sqlite/store.go b/taco/internal/query/sqlite/store.go index b2e24fbd8..75e5ed61a 100644 --- a/taco/internal/query/sqlite/store.go +++ b/taco/internal/query/sqlite/store.go @@ -1,370 +1,44 @@ package sqlite - import ( + "fmt" "os" + "path/filepath" + "strings" + + "github.com/diggerhq/digger/opentaco/internal/query" + "github.com/diggerhq/digger/opentaco/internal/query/common" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" - "path/filepath" - "fmt" - "context" - "log" - "time" - - - "github.com/diggerhq/digger/opentaco/internal/query/types" - ) - -type SQLiteQueryStore struct { - db *gorm.DB - config Config -} - - -type Config struct { - Path string - Models []any - Cache string - EnableForeignKeys bool - EnableWAL bool - BusyTimeout time.Duration - MaxOpenConns int - MaxIdleConns int - ConnMaxLifetime time.Duration -} - -func NewSQLiteQueryStore(cfg Config) (*SQLiteQueryStore, error) { - - //set up SQLite - db, err := openSQLite(cfg) - - if err != nil { - - return nil, fmt.Errorf("Failed to open SQLite: %s", err) - } - - //initialize the store - store := &SQLiteQueryStore{db: db, config: cfg} - - - // migrate the models - if err := store.migrate(); err != nil { - return nil, fmt.Errorf("Failed to migrate store: %w", err) - } - - // create the views for the store - if err := store.createViews(); err != nil { - return nil, fmt.Errorf("Failed to create views for the store: %v", err) +// NewSQLiteQueryStore creates a new SQLite-backed query store. +func NewSQLiteQueryStore(cfg query.SQLiteConfig) (query.Store, error) { + if err := os.MkdirAll(filepath.Dir(cfg.Path), 0755); err != nil { + return nil, fmt.Errorf("create db dir: %v", err) } - log.Printf("SQLite query store successfully initialized: %s", cfg.Path) - - - return store, nil -} - - - -func openSQLite(cfg Config) (*gorm.DB, error){ - - - if cfg.Path == "" { - cfg.Path = "./data/taco.db" - - if err := os.MkdirAll(filepath.Dir(cfg.Path), 0755); err != nil { - return nil, fmt.Errorf("create db dir: %v", err) - } - - } - - - if cfg.Cache == "" { - cfg.Cache = "shared" - } - - if cfg.BusyTimeout == 0 { - cfg.BusyTimeout = 5 * time.Second - } - - if cfg.MaxOpenConns == 0 { - cfg.MaxOpenConns = 1 - } - - if cfg.MaxIdleConns == 0 { - cfg.MaxIdleConns = 1 - } - - - if cfg.PragmaJournalMode == ""{ - cfg.PragmaJournalMode = "WAL" - } - - if cfg.PragmaForeignKeys == "" { - cfg.PragmaForeignKeys = "ON" - } - - if cfg.PragmaBusyTimeout == "" { - cfg.PragmaBusyTimeout = "5000" - } - - // (ConnMaxLifeTime default to 0) - - - dsn := fmt.Sprintf ("file:%s?cache=%v", cfg.Path, cfg.Cache) - + dsn := fmt.Sprintf("file:%s?cache=%s", cfg.Path, cfg.Cache) db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{ - Logger: logger.Default.LogMode(logger.Info), // show SQL while developing + Logger: logger.Default.LogMode(logger.Info), // Or Silent }) if err != nil { - return nil , fmt.Errorf("open sqlite: %v", err) - } - - // Connection pool hints (SQLite is single-writer; 1 open conn is safe) - sqlDB, err := db.DB() - if err != nil { - return nil, fmt.Errorf("unwrap sql.DB: %w", err) + return nil, fmt.Errorf("open sqlite: %v", err) } - sqlDB.SetMaxOpenConns(cfg.MaxOpenConns) - sqlDB.SetMaxIdleConns(cfg.MaxIdleConns) - sqlDB.SetConnMaxLifetime(cfg.ConnMaxLifetime) - + // Apply SQLite-specific PRAGMAs if err := db.Exec(fmt.Sprintf("PRAGMA journal_mode = %s;", strings.ToUpper(cfg.PragmaJournalMode))).Error; err != nil { - return nil, fmt.Errorf("apply journal_mode: %w", err) + return nil, fmt.Errorf("apply journal_mode: %w", err) } - if err := db.Exec(fmt.Sprintf("PRAGMA foreign_keys = %s;", strings.ToUpper(cfg.PragmaForeignKeys))).Error; err != nil { - return nil, fmt.Errorf("apply foreign_keys: %w", err) + return nil, fmt.Errorf("apply foreign_keys: %w", err) } - if err := db.Exec(fmt.Sprintf("PRAGMA busy_timeout = %s;", cfg.PragmaBusyTimeout)).Error; err != nil { - return nil, fmt.Errorf("apply busy_timeout: %w", err) + return nil, fmt.Errorf("apply busy_timeout: %w", err) } - return db, nil - -} - - -func (s *SQLiteQueryStore) migrate() error { - - // expect default models - models := types.DefaultModels - - - // if the models are specified, load them - if len(s.config.Models) > 0 { - models = s.config.Models - } - - if err := s.db.AutoMigrate(models...); err != nil { - return fmt.Errorf("Migration failed: %w", err) - } - - return nil -} - - -// prefix is the location within the bucket like /prod/region1/etc -func (s *SQLiteQueryStore) ListUnits(ctx context.Context, prefix string) ([]types.Unit, error) { - var units []types.Unit - q := s.db.WithContext(ctx).Preload("Tags") - - if prefix != "" { - q = q.Where("name LIKE ?", prefix+"%") - } - - if err := q.Find(&units).Error; err != nil { - return nil, err - } - - return units, nil -} - -func (s *SQLiteQueryStore) GetUnit(ctx context.Context, id string) (*types.Unit, error) { - var unit types.Unit - err := s.db.WithContext(ctx). - Preload("Tags"). - Where("name = ?", id). - First(&unit).Error - - if err != nil { - if err == gorm.ErrRecordNotFound { - return nil, types.ErrNotFound - } - return nil, err - } - - return &unit, nil -} - - - - -func (s *SQLiteQueryStore) IsEnabled() bool { - // not NOOP ? - return true -} - -func (s *SQLiteQueryStore) Close() error { - sqlDB, err := s.db.DB() - if err != nil { - return err - } - return sqlDB.Close() -} - - - -func (s *SQLiteQueryStore) SyncEnsureUnit(ctx context.Context, unitName string) error { - unit := types.Unit{Name: unitName} - return s.db.WithContext(ctx).FirstOrCreate(&unit, types.Unit{Name: unitName}).Error -} - -func (s *SQLiteQueryStore) SyncDeleteUnit(ctx context.Context, unitName string) error { - return s.db.WithContext(ctx).Where("name = ?", unitName).Delete(&types.Unit{}).Error -} - - - - - - -func (s *SQLiteQueryStore) FilterUnitIDsByUser(ctx context.Context, userSubject string, unitIDs []string) ([]string, error) { - -// empty input? - if len(unitIDs) == 0 { - return []string{}, nil - } - - - var allowedUnitIDs []string - - - - err := s.db.WithContext(ctx). - Table("user_unit_access"). - Select("unit_name"). - Where ("user_subject = ?", userSubject). - Where("unit_name IN ?", unitIDs). - Pluck("unit_name", &allowedUnitIDs).Error - - - if err != nil { - return nil, fmt.Errorf("Failed to filter the units by user : %w", err) - - } - - return allowedUnitIDs, nil -} - - - - -func (s *SQLiteQueryStore) ListUnitsForUser(ctx context.Context, userSubject string, prefix string) ([]types.Unit, error) { - var units []types.Unit - - q := s.db.WithContext(ctx). - Table("units"). - Select("units.*"). - Joins("JOIN user_unit_access ON units.name = user_unit_access.unit_name"). - Where("user_unit_access.user_subject = ?", userSubject). - Preload("Tags") - - if prefix != "" { - q = q.Where("units.name LIKE ?", prefix +"%") - } - - - - err:= q.Find(&units).Error - - if err != nil { - return nil, fmt.Errorf("failed to list units for user: %w", err) - } - - return units, nil -} - - -type viewDefinition struct { - name string - sql string -} -// CTE method for user_permissions - gets users with their permission rules -func (s *SQLiteQueryStore) userPermissionsCTE() string { - return ` - SELECT DISTINCT - u.subject as user_subject, - r.id as rule_id, - r.wildcard_resource, - r.effect - FROM users u - JOIN user_roles ur ON u.id = ur.user_id - JOIN role_permissions rp ON ur.role_id = rp.role_id - JOIN rules r ON rp.permission_id = r.permission_id - LEFT JOIN rule_actions ra ON r.id = ra.rule_id - WHERE r.effect = 'allow' - AND (r.wildcard_action = 1 OR ra.action = 'unit.read' OR ra.action IS NULL)` -} - -// CTE method for wildcard_access - handles users with wildcard resource access -func (s *SQLiteQueryStore) wildcardAccessCTE() string { - return ` - SELECT DISTINCT - up.user_subject, - un.name as unit_name - FROM user_permissions up - CROSS JOIN units un - WHERE up.wildcard_resource = 1` -} - -// CTE method for specific_access - handles users with specific unit access -func (s *SQLiteQueryStore) specificAccessCTE() string { - return ` - SELECT DISTINCT - up.user_subject, - un.name as unit_name - FROM user_permissions up - JOIN rule_units ru ON up.rule_id = ru.rule_id - JOIN units un ON ru.unit_id = un.id - WHERE up.wildcard_resource = 0` -} - -// Helper method to create individual views -func (s *SQLiteQueryStore) createView(name, sql string) error { - query := fmt.Sprintf("CREATE VIEW IF NOT EXISTS %s AS %s", name, sql) - return s.db.Exec(query).Error -} - -// Refactored createViews method -func (s *SQLiteQueryStore) createViews() error { - views := []viewDefinition{ - { - name: "user_unit_access", - sql: s.buildUserUnitAccessView(), - }, - } - - for _, view := range views { - if err := s.createView(view.name, view.sql); err != nil { - return fmt.Errorf("failed to create view %s: %w", view.name, err) - } - } - return nil -} - -// Refactored buildUserUnitAccessView method -func (s *SQLiteQueryStore) buildUserUnitAccessView() string { - return ` - WITH user_permissions AS (` + s.userPermissionsCTE() + `), - wildcard_access AS (` + s.wildcardAccessCTE() + `), - specific_access AS (` + s.specificAccessCTE() + `) - SELECT user_subject, unit_name FROM wildcard_access - UNION - SELECT user_subject, unit_name FROM specific_access` + // Create the common SQLStore with our configured DB object, breaking the cycle. + return common.NewSQLStore(db) } diff --git a/taco/internal/query/types/errors.go b/taco/internal/query/types/errors.go index 4956012fe..f45a6f507 100644 --- a/taco/internal/query/types/errors.go +++ b/taco/internal/query/types/errors.go @@ -1,7 +1,6 @@ package types import ( - "errors" ) diff --git a/taco/internal/query/types/models.go b/taco/internal/query/types/models.go index 865f06de6..8b331f51d 100644 --- a/taco/internal/query/types/models.go +++ b/taco/internal/query/types/models.go @@ -80,10 +80,15 @@ type User struct { } type Unit struct { - ID int64 `gorm:"primaryKey"` - Name string `gorm:"uniqueIndex"` - Tags []Tag `gorm:"many2many:unit_tags;constraint:OnDelete:CASCADE,OnUpdate:CASCADE"` - + ID int64 `gorm:"primaryKey"` + Name string `gorm:"uniqueIndex"` + Size int64 `gorm:"default:0"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + Locked bool `gorm:"default:false"` + LockID string `gorm:"default:''"` + LockWho string `gorm:"default:''"` + LockCreated time.Time + Tags []Tag `gorm:"many2many:unit_tags;constraint:OnDelete:CASCADE,OnUpdate:CASCADE"` } type Tag struct { diff --git a/taco/internal/queryfactory/factory.go b/taco/internal/queryfactory/factory.go new file mode 100644 index 000000000..b22fe2db4 --- /dev/null +++ b/taco/internal/queryfactory/factory.go @@ -0,0 +1,33 @@ +// Package queryfactory is responsible for constructing a query.Store. +// It is in a separate package from query to prevent circular dependencies, +// as it needs to import the various database-specific store packages. +package queryfactory + +import ( + "fmt" + "strings" + + "github.com/diggerhq/digger/opentaco/internal/query" + "github.com/diggerhq/digger/opentaco/internal/query/mssql" + "github.com/diggerhq/digger/opentaco/internal/query/mysql" + "github.com/diggerhq/digger/opentaco/internal/query/postgres" + "github.com/diggerhq/digger/opentaco/internal/query/sqlite" +) + +// NewQueryStore creates a new query.Store based on the provided configuration. +func NewQueryStore(cfg query.Config) (query.Store, error) { + backend := strings.ToLower(cfg.Backend) + + switch backend { + case "sqlite", "": + return sqlite.NewSQLiteQueryStore(cfg.SQLite) + case "postgres": + return postgres.NewPostgresStore(cfg.Postgres) + case "mssql": + return mssql.NewMSSQLStore(cfg.MSSQL) + case "mysql": + return mysql.NewMySQLStore(cfg.MySQL) + default: + return nil, fmt.Errorf("unsupported TACO_QUERY_BACKEND value: %q (supported: sqlite, postgres, mssql, mysql)", backend) + } +} diff --git a/taco/internal/rbac/s3store.go b/taco/internal/rbac/s3store.go index e3bf4b159..5ed87b2a3 100644 --- a/taco/internal/rbac/s3store.go +++ b/taco/internal/rbac/s3store.go @@ -15,15 +15,17 @@ import ( "github.com/aws/smithy-go" ) -// s3RBACStore implements RBACStore backed by S3 +// s3RBACStore implements storage.RBACStore backed by S3 type s3RBACStore struct { client *s3.Client bucket string prefix string } -// NewS3RBACStore creates a new S3-backed RBAC store -func NewS3RBACStore(client *s3.Client, bucket, prefix string) RBACStore { +// NewS3RBACStore creates a new S3-backed RBAC store. +// Returns the concrete type which implements both storage.RBACStore (read-only) +// and rbac.RBACStore (full CRUD operations). +func NewS3RBACStore(client *s3.Client, bucket, prefix string) *s3RBACStore { return &s3RBACStore{ client: client, bucket: bucket, @@ -165,45 +167,11 @@ func (s *s3RBACStore) GetPermission(ctx context.Context, id string) (*Permission } func (s *s3RBACStore) ListPermissions(ctx context.Context) ([]*Permission, error) { - permissionsPrefix := s.key("rbac", "permissions") + "/" - - var permissions []*Permission - var token *string - - for { - resp, err := s.client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{ - Bucket: &s.bucket, - Prefix: aws.String(permissionsPrefix), - ContinuationToken: token, - }) - if err != nil { - return nil, err - } - - for _, obj := range resp.Contents { - key := aws.ToString(obj.Key) - if !strings.HasSuffix(key, ".json") { - continue - } - - // Extract permission ID from key - permissionID := strings.TrimSuffix(strings.TrimPrefix(key, permissionsPrefix), ".json") - - permission, err := s.GetPermission(ctx, permissionID) - if err != nil { - continue // Skip invalid permissions - } - permissions = append(permissions, permission) - } - - if aws.ToBool(resp.IsTruncated) && resp.NextContinuationToken != nil { - token = resp.NextContinuationToken - continue - } - break + perms, err := s.listPermissionsTyped(ctx) + if err != nil { + return nil, err } - - return permissions, nil + return perms, nil } func (s *s3RBACStore) DeletePermission(ctx context.Context, id string) error { @@ -287,44 +255,10 @@ func (s *s3RBACStore) GetRole(ctx context.Context, id string) (*Role, error) { } func (s *s3RBACStore) ListRoles(ctx context.Context) ([]*Role, error) { - rolesPrefix := s.key("rbac", "roles") + "/" - - var roles []*Role - var token *string - - for { - resp, err := s.client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{ - Bucket: &s.bucket, - Prefix: aws.String(rolesPrefix), - ContinuationToken: token, - }) - if err != nil { - return nil, err - } - - for _, obj := range resp.Contents { - key := aws.ToString(obj.Key) - if !strings.HasSuffix(key, ".json") { - continue - } - - // Extract role ID from key - roleID := strings.TrimSuffix(strings.TrimPrefix(key, rolesPrefix), ".json") - - role, err := s.GetRole(ctx, roleID) - if err != nil { - continue // Skip invalid roles - } - roles = append(roles, role) - } - - if aws.ToBool(resp.IsTruncated) && resp.NextContinuationToken != nil { - token = resp.NextContinuationToken - continue - } - break + roles, err := s.listRolesTyped(ctx) + if err != nil { + return nil, err } - return roles, nil } @@ -487,8 +421,8 @@ func (s *s3RBACStore) GetUserAssignment(ctx context.Context, subject string) (*U // GetUserAssignmentByEmail finds a user assignment by email address func (s *s3RBACStore) GetUserAssignmentByEmail(ctx context.Context, email string) (*UserAssignment, error) { - // List all user assignments and find by email - assignments, err := s.ListUserAssignments(ctx) + // Use the typed internal method, not the interface{} wrapper + assignments, err := s.listUserAssignmentsTyped(ctx) if err != nil { return nil, err } @@ -503,48 +437,11 @@ func (s *s3RBACStore) GetUserAssignmentByEmail(ctx context.Context, email string } func (s *s3RBACStore) ListUserAssignments(ctx context.Context) ([]*UserAssignment, error) { - usersPrefix := s.key("rbac", "users") + "/" - - var assignments []*UserAssignment - var token *string - - for { - resp, err := s.client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{ - Bucket: &s.bucket, - Prefix: aws.String(usersPrefix), - ContinuationToken: token, - }) - if err != nil { - return nil, err - } - - for _, obj := range resp.Contents { - key := aws.ToString(obj.Key) - if !strings.HasSuffix(key, ".json") { - continue - } - - // Extract subject from key - subject := strings.TrimSuffix(strings.TrimPrefix(key, usersPrefix), ".json") - // Convert back from safe format - subject = strings.ReplaceAll(subject, "_", "/") - subject = strings.ReplaceAll(subject, "_", ":") - - assignment, err := s.GetUserAssignment(ctx, subject) - if err != nil { - continue // Skip invalid assignments - } - assignments = append(assignments, assignment) - } - - if aws.ToBool(resp.IsTruncated) && resp.NextContinuationToken != nil { - token = resp.NextContinuationToken - continue - } - break + users, err := s.listUserAssignmentsTyped(ctx) + if err != nil { + return nil, err } - - return assignments, nil + return users, nil } func (s *s3RBACStore) saveUserAssignment(ctx context.Context, assignment *UserAssignment) error { @@ -590,3 +487,135 @@ func (s *s3RBACStore) saveUserAssignmentWithVersion(ctx context.Context, assignm return err } + +// listPermissionsTyped is the internal typed implementation +func (s *s3RBACStore) listPermissionsTyped(ctx context.Context) ([]*Permission, error) { + permissionsPrefix := s.key("rbac", "permissions") + "/" + + var permissions []*Permission + var token *string + + for { + resp, err := s.client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{ + Bucket: &s.bucket, + Prefix: aws.String(permissionsPrefix), + ContinuationToken: token, + }) + if err != nil { + return nil, err + } + + for _, obj := range resp.Contents { + key := aws.ToString(obj.Key) + if !strings.HasSuffix(key, ".json") { + continue + } + + // Extract permission ID from key + permissionID := strings.TrimSuffix(strings.TrimPrefix(key, permissionsPrefix), ".json") + + permission, err := s.GetPermission(ctx, permissionID) + if err != nil { + continue // Skip invalid permissions + } + permissions = append(permissions, permission) + } + + if aws.ToBool(resp.IsTruncated) && resp.NextContinuationToken != nil { + token = resp.NextContinuationToken + continue + } + break + } + + return permissions, nil +} + +// listRolesTyped is the internal typed implementation +func (s *s3RBACStore) listRolesTyped(ctx context.Context) ([]*Role, error) { + rolesPrefix := s.key("rbac", "roles") + "/" + + var roles []*Role + var token *string + + for { + resp, err := s.client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{ + Bucket: &s.bucket, + Prefix: aws.String(rolesPrefix), + ContinuationToken: token, + }) + if err != nil { + return nil, err + } + + for _, obj := range resp.Contents { + key := aws.ToString(obj.Key) + if !strings.HasSuffix(key, ".json") { + continue + } + + // Extract role ID from key + roleID := strings.TrimSuffix(strings.TrimPrefix(key, rolesPrefix), ".json") + + role, err := s.GetRole(ctx, roleID) + if err != nil { + continue // Skip invalid roles + } + roles = append(roles, role) + } + + if aws.ToBool(resp.IsTruncated) && resp.NextContinuationToken != nil { + token = resp.NextContinuationToken + continue + } + break + } + + return roles, nil +} + +// listUserAssignmentsTyped is the internal typed implementation +func (s *s3RBACStore) listUserAssignmentsTyped(ctx context.Context) ([]*UserAssignment, error) { + usersPrefix := s.key("rbac", "users") + "/" + + var assignments []*UserAssignment + var token *string + + for { + resp, err := s.client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{ + Bucket: &s.bucket, + Prefix: aws.String(usersPrefix), + ContinuationToken: token, + }) + if err != nil { + return nil, err + } + + for _, obj := range resp.Contents { + key := aws.ToString(obj.Key) + if !strings.HasSuffix(key, ".json") { + continue + } + + // Extract subject from key + subject := strings.TrimSuffix(strings.TrimPrefix(key, usersPrefix), ".json") + // Convert back from safe format + subject = strings.ReplaceAll(subject, "_", "/") + subject = strings.ReplaceAll(subject, "_", ":") + + assignment, err := s.GetUserAssignment(ctx, subject) + if err != nil { + continue // Skip invalid assignments + } + assignments = append(assignments, assignment) + } + + if aws.ToBool(resp.IsTruncated) && resp.NextContinuationToken != nil { + token = resp.NextContinuationToken + continue + } + break + } + + return assignments, nil +} diff --git a/taco/internal/storage/authorizer.go b/taco/internal/storage/authorizer.go new file mode 100644 index 000000000..cc666cf60 --- /dev/null +++ b/taco/internal/storage/authorizer.go @@ -0,0 +1,208 @@ +package storage + +import ( + "context" + "errors" + "log" + "time" + + "github.com/diggerhq/digger/opentaco/internal/principal" + "github.com/diggerhq/digger/opentaco/internal/query" +) + +// principalKey is a private type to prevent key collisions in context. +type principalKey string + +const userPrincipalKey principalKey = "user" + +// ContextWithPrincipal returns a new context with the given user principal. +func ContextWithPrincipal(ctx context.Context, p principal.Principal) context.Context { + return context.WithValue(ctx, userPrincipalKey, p) +} + +// principalFromContext retrieves the user principal from the context. +func principalFromContext(ctx context.Context) (principal.Principal, error) { + p := ctx.Value(userPrincipalKey) + if p == nil { + return principal.Principal{}, errors.New("no user principal in context") + } + pr, ok := p.(principal.Principal) + if !ok { + return principal.Principal{}, errors.New("invalid user principal type in context") + } + return pr, nil +} + +// AuthorizingStore is a decorator that enforces role-based access control +// on an underlying UnitStore. +type AuthorizingStore struct { + nextStore UnitStore // The next store in the chain (e.g., OrchestratingStore) + queryStore query.Store // Needed to perform the RBAC checks +} + +// NewAuthorizingStore creates a new store that wraps another with an authorization layer. +func NewAuthorizingStore(next UnitStore, qs query.Store) UnitStore { + return &AuthorizingStore{ + nextStore: next, + queryStore: qs, + } +} + +// List intercepts the call and returns only the units the user is permitted to see. +func (s *AuthorizingStore) List(ctx context.Context, prefix string) ([]*UnitMetadata, error) { + principal, err := principalFromContext(ctx) + if err != nil { + log.Printf("DEBUG AuthorizingStore.List: Failed to get principal from context: %v", err) + return nil, errors.New("unauthorized") + } + + log.Printf("DEBUG AuthorizingStore.List: Got principal: %+v", principal) + + // Use the optimized query that fetches ONLY the units the user is allowed to see. + units, err := s.queryStore.ListUnitsForUser(ctx, principal.Subject, prefix) + if err != nil { + log.Printf("DEBUG AuthorizingStore.List: ListUnitsForUser failed: %v", err) + return nil, err + } + + log.Printf("DEBUG AuthorizingStore.List: Found %d units for user %s", len(units), principal.Subject) + + + metadata := make([]*UnitMetadata, len(units)) + for i, u := range units { + log.Printf("DEBUG: DB Unit: Name=%s, Size=%d, UpdatedAt=%v, Locked=%v", u.Name, u.Size, u.UpdatedAt, u.Locked) + + var lockInfo *LockInfo + if u.Locked { + lockInfo = &LockInfo{ + ID: u.LockID, + Who: u.LockWho, + Created: u.LockCreated, + } + } + metadata[i] = &UnitMetadata{ + ID: u.Name, + Size: u.Size, + Updated: u.UpdatedAt, + Locked: u.Locked, + LockInfo: lockInfo, + } + log.Printf("DEBUG: Mapped Metadata: ID=%s, Size=%d, Updated=%v", metadata[i].ID, metadata[i].Size, metadata[i].Updated) + } + + return metadata, nil +} + +// checkPermission is a new helper to centralize permission checks. +func (s *AuthorizingStore) checkPermission(ctx context.Context, action, unitID string) error { + principal, err := principalFromContext(ctx) + if err != nil { + return errors.New("unauthorized") + } + + allowed, err := s.queryStore.CanPerformAction(ctx, principal.Subject, action, unitID) + if err != nil { + log.Printf("RBAC check failed for user '%s', action '%s' on unit '%s': %v", principal.Subject, action, unitID, err) + return errors.New("internal authorization error") + } + if !allowed { + return errors.New("forbidden") + } + return nil +} + +// Get checks for 'unit.read' permission. +func (s *AuthorizingStore) Get(ctx context.Context, id string) (*UnitMetadata, error) { + if err := s.checkPermission(ctx, "unit.read", id); err != nil { + return nil, err + } + return s.nextStore.Get(ctx, id) +} + +// Download checks for 'unit.read' permission. +func (s *AuthorizingStore) Download(ctx context.Context, id string) ([]byte, error) { + if err := s.checkPermission(ctx, "unit.read", id); err != nil { + return nil, err + } + return s.nextStore.Download(ctx, id) +} + +// Create checks for 'unit.write' permission. +func (s *AuthorizingStore) Create(ctx context.Context, id string) (*UnitMetadata, error) { + if err := s.checkPermission(ctx, "unit.write", id); err != nil { + return nil, err + } + return s.nextStore.Create(ctx, id) +} + +// Upload checks for 'unit.write' permission. +func (s *AuthorizingStore) Upload(ctx context.Context, id string, data []byte, lockID string) error { + if err := s.checkPermission(ctx, "unit.write", id); err != nil { + return err + } + return s.nextStore.Upload(ctx, id, data, lockID) +} + +// Delete checks for 'unit.delete' permission. +func (s *AuthorizingStore) Delete(ctx context.Context, id string) error { + if err := s.checkPermission(ctx, "unit.delete", id); err != nil { + return err + } + return s.nextStore.Delete(ctx, id) +} + +// Lock checks for 'unit.lock' permission. +func (s *AuthorizingStore) Lock(ctx context.Context, id string, info *LockInfo) error { + if err := s.checkPermission(ctx, "unit.lock", id); err != nil { + return err + } + err := s.nextStore.Lock(ctx, id, info) + if err != nil { + return err + } + + // Sync lock status to database + if err := s.queryStore.SyncUnitLock(ctx, id, info.ID, info.Who, info.Created); err != nil { + log.Printf("Warning: Failed to sync lock status for unit '%s': %v", id, err) + } + return nil +} + +// Unlock checks for 'unit.lock' permission. +func (s *AuthorizingStore) Unlock(ctx context.Context, id string, lockID string) error { + if err := s.checkPermission(ctx, "unit.lock", id); err != nil { + return err + } + err := s.nextStore.Unlock(ctx, id, lockID) + if err != nil { + return err + } + + // Sync unlock status to database + if err := s.queryStore.SyncUnitUnlock(ctx, id); err != nil { + log.Printf("Warning: Failed to sync unlock status for unit '%s': %v", id, err) + } + return nil +} + +// --- Other Pass-through Methods with Read Checks --- +func (s *AuthorizingStore) GetLock(ctx context.Context, id string) (*LockInfo, error) { + if err := s.checkPermission(ctx, "unit.read", id); err != nil { + return nil, err + } + return s.nextStore.GetLock(ctx, id) +} + +func (s *AuthorizingStore) ListVersions(ctx context.Context, id string) ([]*VersionInfo, error) { + if err := s.checkPermission(ctx, "unit.read", id); err != nil { + return nil, err + } + return s.nextStore.ListVersions(ctx, id) +} + +func (s *AuthorizingStore) RestoreVersion(ctx context.Context, id string, versionTimestamp time.Time, lockID string) error { + if err := s.checkPermission(ctx, "unit.write", id); err != nil { + return err + } + return s.nextStore.RestoreVersion(ctx, id, versionTimestamp, lockID) +} \ No newline at end of file diff --git a/taco/internal/storage/interface.go b/taco/internal/storage/interface.go index 6d64e4d58..265839e41 100644 --- a/taco/internal/storage/interface.go +++ b/taco/internal/storage/interface.go @@ -55,9 +55,10 @@ type UnitStore interface { // Version operations ListVersions(ctx context.Context, id string) ([]*VersionInfo, error) RestoreVersion(ctx context.Context, id string, versionTimestamp time.Time, lockID string) error + } -// S3Store extends UnitStore with S3-specific methods for RBAC integration +// S3Store extends UnitStore with S3-specific accessors for integration type S3Store interface { UnitStore GetS3Client() *s3.Client diff --git a/taco/internal/storage/orchestrator.go b/taco/internal/storage/orchestrator.go new file mode 100644 index 000000000..0126a5aef --- /dev/null +++ b/taco/internal/storage/orchestrator.go @@ -0,0 +1,156 @@ +package storage + +import ( + "context" + "log" + "time" + + "github.com/diggerhq/digger/opentaco/internal/query" + "github.com/diggerhq/digger/opentaco/internal/query/types" +) + +// OrchestratingStore implements the UnitStore interface to coordinate a blob store +// (like S3) and a database index (like SQLite). +type OrchestratingStore struct { + blobStore UnitStore // The primary source of truth for file content (e.g., S3Store) + queryStore query.Store // The source of truth for metadata and listings +} + +// NewOrchestratingStore creates a new store that synchronizes blob and database storage. +func NewOrchestratingStore(blobStore UnitStore, queryStore query.Store) UnitStore { + return &OrchestratingStore{ + blobStore: blobStore, + queryStore: queryStore, + } +} + +// Create writes to the blob store first, then syncs the metadata to the database. +func (s *OrchestratingStore) Create(ctx context.Context, id string) (*UnitMetadata, error) { + meta, err := s.blobStore.Create(ctx, id) + if err != nil { + return nil, err // If blob storage fails, the whole operation fails. + } + + if err := s.queryStore.SyncEnsureUnit(ctx, id); err != nil { + log.Printf("CRITICAL: Unit '%s' created in blob storage but failed to sync to database: %v", id, err) + } else { + // Sync metadata too + if err := s.queryStore.SyncUnitMetadata(ctx, id, meta.Size, meta.Updated); err != nil { + log.Printf("Warning: Failed to sync metadata for unit '%s': %v", id, err) + } + } + return meta, nil +} + +// Upload writes to the blob store first, then syncs the metadata to the database. +func (s *OrchestratingStore) Upload(ctx context.Context, id string, data []byte, lockID string) error { + err := s.blobStore.Upload(ctx, id, data, lockID) + if err != nil { + return err + } + + // Get metadata to sync size + meta, err := s.blobStore.Get(ctx, id) + if err == nil && s.queryStore.IsEnabled() { + // Sync with full metadata + if syncer, ok := s.queryStore.(interface { + SyncUnitMetadata(context.Context, string, int64, time.Time) error + }); ok { + syncer.SyncUnitMetadata(ctx, id, meta.Size, meta.Updated) + } + } + + return nil +} + +// Delete removes from the blob store first, then syncs the deletion to the database. +func (s *OrchestratingStore) Delete(ctx context.Context, id string) error { + err := s.blobStore.Delete(ctx, id) + if err != nil { + return err + } + if err := s.queryStore.SyncDeleteUnit(ctx, id); err != nil { + log.Printf("CRITICAL: Unit '%s' deleted from blob storage but failed to sync to database: %v", id, err) + } + return nil +} + +// List bypasses blob storage and uses the fast database index. +func (s *OrchestratingStore) List(ctx context.Context, prefix string) ([]*UnitMetadata, error) { + var units []types.Unit + units, err := s.queryStore.ListUnits(ctx, prefix) + if err != nil { + return nil, err + } + + // Adapt the result from the query store's type to the storage's type. + metadata := make([]*UnitMetadata, len(units)) + for i, u := range units { + var lockInfo *LockInfo + if u.Locked { + lockInfo = &LockInfo{ + ID: u.LockID, + Who: u.LockWho, + Created: u.LockCreated, + } + } + metadata[i] = &UnitMetadata{ + ID: u.Name, + Size: u.Size, + Updated: u.UpdatedAt, + Locked: u.Locked, + LockInfo: lockInfo, + } + } + return metadata, nil +} + +// --- Pass-through methods --- +// For operations that only concern the blob data itself, we pass them directly +// to the underlying blob store. + +func (s *OrchestratingStore) Get(ctx context.Context, id string) (*UnitMetadata, error) { + return s.blobStore.Get(ctx, id) +} + +func (s *OrchestratingStore) Download(ctx context.Context, id string) ([]byte, error) { + return s.blobStore.Download(ctx, id) +} + +func (s *OrchestratingStore) Lock(ctx context.Context, id string, info *LockInfo) error { + err := s.blobStore.Lock(ctx, id, info) + if err != nil { + return err + } + + // Sync lock status to database + if err := s.queryStore.SyncUnitLock(ctx, id, info.ID, info.Who, info.Created); err != nil { + log.Printf("Warning: Failed to sync lock status for unit '%s': %v", id, err) + } + return nil +} + +func (s *OrchestratingStore) Unlock(ctx context.Context, id string, lockID string) error { + err := s.blobStore.Unlock(ctx, id, lockID) + if err != nil { + return err + } + + // Sync unlock status to database + if err := s.queryStore.SyncUnitUnlock(ctx, id); err != nil { + log.Printf("Warning: Failed to sync unlock status for unit '%s': %v", id, err) + } + return nil +} + +func (s *OrchestratingStore) GetLock(ctx context.Context, id string) (*LockInfo, error) { + return s.blobStore.GetLock(ctx, id) +} + +func (s *OrchestratingStore) ListVersions(ctx context.Context, id string) ([]*VersionInfo, error) { + return s.blobStore.ListVersions(ctx, id) +} + +func (s *OrchestratingStore) RestoreVersion(ctx context.Context, id string, versionTimestamp time.Time, lockID string) error { + return s.blobStore.RestoreVersion(ctx, id, versionTimestamp, lockID) +} \ No newline at end of file diff --git a/taco/internal/unit/handler.go b/taco/internal/unit/handler.go index 28956c733..e9f832378 100644 --- a/taco/internal/unit/handler.go +++ b/taco/internal/unit/handler.go @@ -13,7 +13,6 @@ import ( "github.com/diggerhq/digger/opentaco/internal/rbac" "github.com/diggerhq/digger/opentaco/internal/storage" "github.com/diggerhq/digger/opentaco/internal/query" - "github.com/diggerhq/digger/opentaco/internal/query/types" "github.com/google/uuid" "github.com/labstack/echo/v4" "log" @@ -70,80 +69,42 @@ func (h *Handler) CreateUnit(c echo.Context) error { return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to create unit"}) } - // POC - write to db example - // if h.db != nil { - // if err := db.SyncCreateUnit(h.db, id); err != nil { - // log.Printf("Warning: failed to sync unit creation to database: %v", err) - // // Don't fail the request if DB sync fails - // } - // } - analytics.SendEssential("unit_created") return c.JSON(http.StatusCreated, CreateUnitResponse{ID: metadata.ID, Created: metadata.Updated}) } func (h *Handler) ListUnits(c echo.Context) error { - ctx := c.Request().Context() - prefix := c.QueryParam("prefix") - - - if h.queryStore.IsEnabled() { - units, err := h.queryStore.ListUnits(ctx, prefix) - if err == nil { - // Index by ID - unitIDs := make([]string, len(units)) - unitMap := make(map[string]types.Unit, len(units)) - for i, u := range units { - unitIDs[i] = u.Name - unitMap[u.Name] = u - } - - if h.rbacManager != nil && h.signer != nil { - principal, perr := h.getPrincipalFromToken(c) - if perr != nil { - // If RBAC is enabled, return 401; otherwise skip RBAC - if enabled, _ := h.rbacManager.IsEnabled(ctx); enabled { - return c.JSON(http.StatusUnauthorized, map[string]string{ - "error": "Failed to authenticate user", - }) - } - } else { - filteredIDs, ferr := h.queryStore.FilterUnitIDsByUser(ctx, principal.Subject, unitIDs) - if ferr != nil { - log.Printf("RBAC filtering via query-store failed; falling back to storage: %v", ferr) - return h.listFromStorage(ctx, c, prefix) - } - unitIDs = filteredIDs - } - } - - // Build response from filtered IDs - domainUnits := make([]*domain.Unit, 0, len(unitIDs)) - for _, id := range unitIDs { - if u, ok := unitMap[id]; ok { - tagNames := make([]string, len(u.Tags)) - for i, t := range u.Tags { - tagNames[i] = t.Name - } - domainUnits = append(domainUnits, &domain.Unit{ - ID: u.Name, - Tags: tagNames, - }) - } - } - domain.SortUnitsByID(domainUnits) - - return c.JSON(http.StatusOK, map[string]interface{}{ - "units": domainUnits, - "count": len(domainUnits), - "source": "query-store", - }) - } - - log.Printf("Query-store ListUnits failed; falling back to storage: %v", err) - } - - return h.listFromStorage(ctx, c, prefix) + ctx := c.Request().Context() + prefix := c.QueryParam("prefix") + + // The RBAC logic is GONE. We just call the store. + // The store (AuthorizingStore) returns a pre-filtered list or an error. + unitsMetadata, err := h.store.List(ctx, prefix) + if err != nil { + if err.Error() == "unauthorized" || err.Error() == "forbidden" { + return c.JSON(http.StatusForbidden, map[string]string{"error": err.Error()}) + } + log.Printf("Error listing units: %v", err) + return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to list units"}) + } + + // The list is already filtered and secure. We just build the response. + domainUnits := make([]*domain.Unit, 0, len(unitsMetadata)) + for _, u := range unitsMetadata { + domainUnits = append(domainUnits, &domain.Unit{ + ID: u.ID, + Size: u.Size, + Updated: u.Updated, + Locked: u.Locked, + LockInfo: convertLockInfo(u.LockInfo), + }) + } + domain.SortUnitsByID(domainUnits) + + return c.JSON(http.StatusOK, map[string]interface{}{ + "units": domainUnits, + "count": len(domainUnits), + }) } // listFromStorage encapsulates the old storage-based path (including RBAC). @@ -211,8 +172,12 @@ func (h *Handler) GetUnit(c echo.Context) error { if err := domain.ValidateUnitID(id); err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()}) } + metadata, err := h.store.Get(c.Request().Context(), id) if err != nil { + if err.Error() == "forbidden" { + return c.JSON(http.StatusForbidden, map[string]string{"error": "Forbidden"}) + } if err == storage.ErrNotFound { return c.JSON(http.StatusNotFound, map[string]string{"error": "Unit not found"}) } @@ -234,14 +199,6 @@ func (h *Handler) DeleteUnit(c echo.Context) error { return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to delete unit"}) } - // // POC - write to db example - // if h.db != nil { - // if err := db.SyncDeleteUnit(h.db, id); err != nil { - // log.Printf("Warning: failed to sync unit deletion to database: %v", err) - // // Don't fail the request if DB sync fails - // } - // } - return c.NoContent(http.StatusNoContent) } @@ -292,12 +249,7 @@ func (h *Handler) UploadUnit(c echo.Context) error { } return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to upload unit"}) } - // POC - write to db - // if h.db != nil { - // if err := db.SyncUnitExists(h.db, id); err != nil { - // log.Printf("Warning: failed to sync unit to database: %v", err) - // } - // } + // Best-effort dependency graph update go deps.UpdateGraphOnWrite(c.Request().Context(), h.store, id, data) analytics.SendEssential("taco_unit_push_completed") diff --git a/taco/internal/wiring/rbac.go b/taco/internal/wiring/rbac.go new file mode 100644 index 000000000..52ee3b1a6 --- /dev/null +++ b/taco/internal/wiring/rbac.go @@ -0,0 +1,69 @@ +package wiring + +import ( + "context" + "log" + + "github.com/diggerhq/digger/opentaco/internal/query" + "github.com/diggerhq/digger/opentaco/internal/rbac" + "github.com/diggerhq/digger/opentaco/internal/storage" +) + +// SyncRBACFromStorage syncs RBAC data from storage to the query database. +// This is called at startup to populate the database with roles, permissions, and users. +func SyncRBACFromStorage(ctx context.Context, store storage.UnitStore, queryStore query.Store) error { + // Check if it's S3 storage + s3Store, ok := store.(storage.S3Store) + if !ok { + log.Println("RBAC sync skipped: storage backend does not support RBAC") + return nil + } + + // Create the S3 RBAC store + rbacStore := rbac.NewS3RBACStore( + s3Store.GetS3Client(), + s3Store.GetS3Bucket(), + s3Store.GetS3Prefix(), + ) + + log.Println("Starting RBAC data sync from S3 to database...") + + // Sync permissions + permissions, err := rbacStore.ListPermissions(ctx) + if err != nil { + return err + } + for _, perm := range permissions { + if err := queryStore.SyncPermission(ctx, perm); err != nil { + log.Printf("Warning: Failed to sync permission %s: %v", perm.ID, err) + } + } + log.Printf("Synced %d permissions", len(permissions)) + + // Sync roles + roles, err := rbacStore.ListRoles(ctx) + if err != nil { + return err + } + for _, role := range roles { + if err := queryStore.SyncRole(ctx, role); err != nil { + log.Printf("Warning: Failed to sync role %s: %v", role.ID, err) + } + } + log.Printf("Synced %d roles", len(roles)) + + // Sync users + users, err := rbacStore.ListUserAssignments(ctx) + if err != nil { + return err + } + for _, user := range users { + if err := queryStore.SyncUser(ctx, user); err != nil { + log.Printf("Warning: Failed to sync user %s: %v", user.Subject, err) + } + } + log.Printf("Synced %d user assignments", len(users)) + + log.Println("RBAC data sync completed successfully") + return nil +} From 5590f43165ed2b1fad6e38ba75ce1dfe1e6919df Mon Sep 17 00:00:00 2001 From: Brian Reardon Date: Mon, 6 Oct 2025 19:47:43 -0700 Subject: [PATCH 07/13] remove duplicates --- taco/cmd/statesman/main.go | 4 - taco/cmd/taco/commands/unit.go | 32 --- taco/internal/db/handler.go | 346 ------------------------ taco/internal/db/helpers.go | 246 ----------------- taco/internal/db/queries.go | 90 ------ taco/internal/middleware/auth.go | 8 - taco/internal/query/common/sql_store.go | 9 +- taco/internal/storage/authorizer.go | 10 - taco/internal/unit/handler.go | 59 ---- 9 files changed, 1 insertion(+), 803 deletions(-) delete mode 100644 taco/internal/db/handler.go delete mode 100644 taco/internal/db/helpers.go delete mode 100644 taco/internal/db/queries.go diff --git a/taco/cmd/statesman/main.go b/taco/cmd/statesman/main.go index d3ca93425..99b81135e 100644 --- a/taco/cmd/statesman/main.go +++ b/taco/cmd/statesman/main.go @@ -109,10 +109,7 @@ func main() { if err != nil { log.Printf("Warning: Failed to list units from storage: %v", err) } else { - log.Printf("DEBUG: Got %d units from storage", len(units)) for _, unit := range units { - log.Printf("DEBUG: Unit from storage: ID=%s, Size=%d, Updated=%v", unit.ID, unit.Size, unit.Updated) - // Always ensure unit exists first if err := queryStore.SyncEnsureUnit(context.Background(), unit.ID); err != nil { log.Printf("Warning: Failed to sync unit %s: %v", unit.ID, err) @@ -120,7 +117,6 @@ func main() { } // Always sync metadata to update existing records - log.Printf("Syncing metadata for %s: size=%d, updated=%v", unit.ID, unit.Size, unit.Updated) if err := queryStore.SyncUnitMetadata(context.Background(), unit.ID, unit.Size, unit.Updated); err != nil { log.Printf("Warning: Failed to sync metadata for unit %s: %v", unit.ID, err) } diff --git a/taco/cmd/taco/commands/unit.go b/taco/cmd/taco/commands/unit.go index cb604bfa5..1326523cc 100644 --- a/taco/cmd/taco/commands/unit.go +++ b/taco/cmd/taco/commands/unit.go @@ -186,38 +186,6 @@ var unitListCmd = &cobra.Command{ }, } -var unitLsFastCmd = &cobra.Command{ - Use: "ls-fast [prefix]", - Short: "List units using database (POC)", - Long: "List units using database lookups instead of S3 for RBAC resolution - proof of concept", - Args: cobra.MaximumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - - - client := newAuthedClient() - - - prefix := "" - if len(args) > 0 { - prefix = args[0] - } - - result, err := client.ListUnitsFast(context.Background(), prefix) - if err != nil { - return fmt.Errorf("failed to list units: %w", err) - } - - // Display results with POC indicator - fmt.Printf("Units (via %s): %d\n", result.Source, result.Count) - for _, unit := range result.Units { - fmt.Printf(" %s\n", unit.ID) - } - - return nil - }, -} - - var unitInfoCmd = &cobra.Command{ Use: "info ", Short: "Show unit metadata information", diff --git a/taco/internal/db/handler.go b/taco/internal/db/handler.go deleted file mode 100644 index b2a26f8f9..000000000 --- a/taco/internal/db/handler.go +++ /dev/null @@ -1,346 +0,0 @@ -package db - -import ( - "log" - "time" - "gorm.io/gorm" - "gorm.io/driver/sqlite" - "gorm.io/gorm/logger" - "github.com/diggerhq/digger/opentaco/internal/storage" - rbac "github.com/diggerhq/digger/opentaco/internal/rbac" - "context" - "os" - "path/filepath" - -) - - -type Role struct { - ID int64 `gorm:"primaryKey"` - RoleId string `gorm:"not null;uniqueIndex"`// like "admin" - Name string //" admin role" - Description string // "Admin Role with full access" - Permissions []Permission `gorm:"many2many:role_permissions;constraint:OnDelete:CASCADE,OnUpdate:CASCADE"` - CreatedAt time.Time//timestamp - CreatedBy string //subject of creator (self for admin) -} - - - - -type Permission struct { - ID int64 `gorm:"primaryKey"` - PermissionId string `gorm:"not null;uniqueIndex"` - Name string // "admin permission" - Description string // "Admin permission allowing all action" - Rules []Rule `gorm:"constraint:OnDelete:CASCADE"` // [{"actions":["unit.read","unit.write","unit.lock","unit.delete","rbac.manage"],"resources":["*"],"effect":"allow"}] FK - CreatedBy string // subject of creator (self for admin) - CreatedAt time.Time -} - -type Rule struct { - ID int64 `gorm:"primaryKey"` - PermissionID int64 `gorm:"index;not null"` - Effect string `gorm:"size:8;not null;default:allow"` // "allow" | "deny" - WildcardAction bool `gorm:"not null;default:false"` - WildcardResource bool `gorm:"not null;default:false"` - Actions []RuleAction `gorm:"constraint:OnDelete:CASCADE"` - UnitTargets []RuleUnit `gorm:"constraint:OnDelete:CASCADE"` - TagTargets []RuleUnitTag `gorm:"constraint:OnDelete:CASCADE"` -} - - - -type RuleAction struct { - ID int64 `gorm:"primaryKey"` - RuleID int64 `gorm:"index;not null"` - Action string `gorm:"size:128;not null;index"` - // UNIQUE (rule_id, action) -} -func (RuleAction) TableName() string { return "rule_actions" } - -type RuleUnit struct { - ID int64 `gorm:"primaryKey"` - RuleID int64 `gorm:"index;not null"` - UnitID int64 `gorm:"index;not null"` - // UNIQUE (rule_id, resource_id) -} -func (RuleUnit) TableName() string { return "rule_units" } - -type RuleUnitTag struct { - ID int64 `gorm:"primaryKey"` - RuleID int64 `gorm:"index;not null"` - TagID int64 `gorm:"index;not null"` - // UNIQUE (rule_id, tag_id) -} -func (RuleUnitTag) TableName() string { return "rule_unit_tags" } - - - - -type User struct { - ID int64 `gorm:"primaryKey"` - Subject string `gorm:"not null;uniqueIndex"` - Email string `gorm:"not nulll;uniqueIndex"` - Roles []Role `gorm:"many2many:user_roles;constraint:OnDelete:CASCADE,OnUpdate:CASCADE"` - CreatedAt time.Time - UpdatedAt time.Time - Version int64 //"1" -} - -type Unit struct { - ID int64 `gorm:"primaryKey"` - Name string `gorm:"uniqueIndex"` - Tags []Tag `gorm:"many2many:unit_tags;constraint:OnDelete:CASCADE,OnUpdate:CASCADE"` - -} - -type Tag struct { - ID int64 `gorm:"primaryKey"` - Name string `gorm:"uniqueIndex"` - -} - - -//explicit joins - - -type UnitTag struct { - UnitID int64 `gorm:"primaryKey;index"` - TagID int64 `gorm:"primaryKey;index"` -} -func (UnitTag) TableName() string { return "unit_tags" } - - -type UserRole struct { - UserID int64 `gorm:"primaryKey;index"` - RoleID int64 `gorm:"primaryKey;index"` -} -func (UserRole) TableName() string { return "user_roles" } - - - -type RolePermission struct { - RoleID int64 `gorm:"primaryKey;index"` - PermissionID int64 `gorm:"primaryKey;index"` -} - - -func (RolePermission) TableName() string { return "role_permissions" } -/* - -todo - -ingest s3 -make adapter so this can be used -make UNIT LS look up with this sytem in the adapter as simple POC - - -*/ - - -var DefaultModels = []any{ - &User{}, - &Role{}, - &UserRole{}, - &Permission{}, - &Rule{}, - &RuleAction{}, - &RuleUnit{}, - &RuleUnitTag{}, - &RolePermission{}, - &Unit{}, - &Tag{}, - &UnitTag{}, -} - -type DBConfig struct { - Path string - Models []any -} - - -func OpenSQLite(cfg DBConfig) *gorm.DB { - - if cfg.Path == "" { - cfg.Path = "./data/taco.db" - - - if err := os.MkdirAll(filepath.Dir(cfg.Path), 0755); err != nil { - log.Fatalf("create db dir: %v", err) - } - - - - } - if len(cfg.Models) == 0 { cfg.Models = DefaultModels } - - // Keep DSN simple; set PRAGMAs via Exec (works reliably across drivers). - dsn := "file:" + cfg.Path + "?cache=shared" - - db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{ - Logger: logger.Default.LogMode(logger.Info), // show SQL while developing - }) - if err != nil { - log.Fatalf("open sqlite: %v", err) - } - - // Connection pool hints (SQLite is single-writer; 1 open conn is safe) - sqlDB, err := db.DB() - if err != nil { - log.Fatalf("unwrap sql.DB: %v", err) - } - sqlDB.SetMaxOpenConns(1) - sqlDB.SetMaxIdleConns(1) - sqlDB.SetConnMaxLifetime(0) - - // Helpful PRAGMAs - if err := db.Exec(` - PRAGMA journal_mode=WAL; - PRAGMA foreign_keys=ON; - PRAGMA busy_timeout=5000; - `).Error; err != nil { - log.Fatalf("pragmas: %v", err) - } - - // AutoMigrate your models (add them below or pass via args) - if err := db.AutoMigrate(cfg.Models...); err != nil { - log.Fatalf("automigrate: %v", err) - } - - // Create the user-unit access view for fast ls-fast lookups - if err := db.Exec(` - CREATE VIEW IF NOT EXISTS user_unit_access AS - WITH user_permissions AS ( - SELECT DISTINCT - u.subject as user_subject, - r.id as rule_id, - r.wildcard_resource, - r.effect - FROM users u - JOIN user_roles ur ON u.id = ur.user_id - JOIN role_permissions rp ON ur.role_id = rp.role_id - JOIN rules r ON rp.permission_id = r.permission_id - LEFT JOIN rule_actions ra ON r.id = ra.rule_id - WHERE r.effect = 'allow' - AND (r.wildcard_action = 1 OR ra.action = 'unit.read' OR ra.action IS NULL) - ), - wildcard_access AS ( - SELECT DISTINCT - up.user_subject, - un.name as unit_name - FROM user_permissions up - CROSS JOIN units un - WHERE up.wildcard_resource = 1 - ), - specific_access AS ( - SELECT DISTINCT - up.user_subject, - un.name as unit_name - FROM user_permissions up - JOIN rule_units ru ON up.rule_id = ru.rule_id - JOIN units un ON ru.unit_id = un.id - WHERE up.wildcard_resource = 0 - ) - SELECT user_subject, unit_name FROM wildcard_access - UNION - SELECT user_subject, unit_name FROM specific_access; - `).Error; err != nil { - log.Printf("Warning: failed to create user_unit_access view: %v", err) - } - - - return db -} - - - - - - -// should make an adapter for this process, but for POC just s3store -func Seed(ctx context.Context, store storage.S3Store, db *gorm.DB){ - - - //gets called from service boot - - // call store - //for each document location - //get all the units TODO: consider tags - allUnits, err := store.List(ctx, "") - - if err != nil { - log.Fatal(err) - } - - //go through each unit - // should batch or use iter for scale - but proof of concept - // pagination via s3store would be trivial - for _, unit := range allUnits { - // create records - r := Unit{Name: unit.ID} - if err := db.FirstOrCreate(&r, Unit{Name: unit.ID}).Error; err != nil { - // if existed, r is loaded; else it’s created - log.Printf("Failed to create or find unit %s: %v", unit.ID, err) - continue - } - } - - // Right now there is no RBAC adapter either, outside of POC should actually implement this as well - S3RBACStore := rbac.NewS3RBACStore(store.GetS3Client(), store.GetS3Bucket(), store.GetS3Prefix()) - - - - //permission - permissions, err := S3RBACStore.ListPermissions(ctx) - if err != nil { - log.Fatal(err) - } - for _, permission := range permissions { - err := SeedPermission(ctx, db, permission) - if err != nil{ - log.Printf("Failed to seed permission: %s", permission.ID) - continue - } - } - - - //roles - roles, err := S3RBACStore.ListRoles(ctx) - if err != nil { - log.Fatal(err) - } - for _, role := range roles { - err := SeedRole(ctx, db, role) - if err != nil { - log.Printf("Failed to seed role: %s", role.ID) - continue - } - } - - - - //users - users, err := S3RBACStore.ListUserAssignments(ctx) - if err != nil { - log.Fatal(err) - } - for _, user := range users { - err := SeedUser(ctx,db,user) - if err != nil { - log.Printf("Failed to seed user: %s", user.Subject) - continue - } - - } - - - //TBD - //TFE tokens. - //system id section - //audit logs - //etc - - - -} diff --git a/taco/internal/db/helpers.go b/taco/internal/db/helpers.go deleted file mode 100644 index cca7981cb..000000000 --- a/taco/internal/db/helpers.go +++ /dev/null @@ -1,246 +0,0 @@ -package db - -import ( - "context" - "fmt" - "strings" - "gorm.io/gorm" - "gorm.io/gorm/clause" - "time" - - rbac "github.com/diggerhq/digger/opentaco/internal/rbac" -) - - -type S3RoleDoc struct { - ID string `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - Permissions []string `json:"permissions"` - CreatedAt time.Time `json:"created_at"` - CreatedBy string `json:"created_by"` - Version int64 `json:"version"` -} - - -type S3UserDoc struct { - Subject string `json:"subject"` - Email string `json:"email"` - Roles []string `json:"roles"` // e.g., ["admin","brian1-developer"] - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - Version int64 `json:"version"` -} - - - -func hasStarResource(list []string) bool { - for _, s := range list { if s == "*" { return true } } - return false -} - -func hasStarAction(list []rbac.Action) bool { - for _, s := range list { if string(s) == "*" { return true } } - return false -} - -func SeedPermission(ctx context.Context, db *gorm.DB, s3Perm *rbac.Permission) error { - - - p := Permission{ - PermissionId: s3Perm.ID, - Name: s3Perm.Name, - Description: s3Perm.Description, - CreatedBy: s3Perm.CreatedBy, - CreatedAt: s3Perm.CreatedAt, - } - if err := db.WithContext(ctx).Clauses(clause.OnConflict{ - Columns: []clause.Column{{Name: "permission_id"}}, - DoUpdates: clause.AssignmentColumns([]string{"name", "description", "created_by"}), - }).Create(&p).Error; err != nil { - return fmt.Errorf("permission upsert %s: %w", s3Perm.ID, err) - } - - // 2) Replace rules (simple + idempotent for seeds) - if err := db.WithContext(ctx). - Where("permission_id = ?", p.ID). - Delete(&Rule{}).Error; err != nil { - return fmt.Errorf("clear rules %s: %w", s3Perm.ID, err) - } - - for _, rr := range s3Perm.Rules { - rule := Rule{ - PermissionID: p.ID, - Effect: strings.ToLower(rr.Effect), - WildcardAction: hasStarAction(rr.Actions), - WildcardResource: hasStarResource(rr.Resources), - } - if err := db.WithContext(ctx).Create(&rule).Error; err != nil { - return fmt.Errorf("create rule: %w", err) - } - - // Only create children if not wildcard - if !rule.WildcardAction { - rows := make([]RuleAction, 0, len(rr.Actions)) - for _, a := range rr.Actions { - rows = append(rows, RuleAction{RuleID: rule.ID, Action: string(a)}) - } - if len(rows) > 0 { - if err := db.WithContext(ctx).Create(&rows).Error; err != nil { - return fmt.Errorf("actions: %w", err) - } - } - } - if !rule.WildcardResource { - // Resolve unit names -> Unit IDs, creating Units if missing - us := make([]RuleUnit, 0, len(rr.Resources)) - for _, name := range rr.Resources { - var u Unit - if err := db.WithContext(ctx). - Where(&Unit{Name: name}). - FirstOrCreate(&u).Error; err != nil { - return fmt.Errorf("ensure unit %q: %w", name, err) - } - us = append(us, RuleUnit{RuleID: rule.ID, UnitID: u.ID}) - } - if len(us) > 0 { - if err := db.WithContext(ctx).Create(&us).Error; err != nil { - return fmt.Errorf("units: %w", err) - } - } - } - } - return nil -} - - - - - - -func SeedRole(ctx context.Context, db *gorm.DB, rbacRole *rbac.Role) error { - return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { - // 1) Upsert role by RoleId - var role Role - if err := tx. - Where(&Role{RoleId: rbacRole.ID}). - Attrs(Role{ - Name: rbacRole.Name, - Description: rbacRole.Description, - CreatedBy: rbacRole.CreatedBy, - CreatedAt: rbacRole.CreatedAt, // keep if you want to trust S3 timestamp - }). - FirstOrCreate(&role).Error; err != nil { - return fmt.Errorf("upsert role %q: %w", rbacRole.ID, err) - } - - // 2) Ensure all permissions exist (by PermissionId) - perms := make([]Permission, 0, len(rbacRole.Permissions)) - if len(rbacRole.Permissions) > 0 { - // fetch existing - var existing []Permission - if err := tx. - Where("permission_id IN ?", rbacRole.Permissions). - Find(&existing).Error; err != nil { - return fmt.Errorf("lookup permissions for role %q: %w", rbacRole.ID, err) - } - - exists := map[string]Permission{} - for _, p := range existing { - exists[p.PermissionId] = p - } - - // create any missing (minimal rows; names can be filled by permission seeder later) - for _, pid := range rbacRole.Permissions { - if p, ok := exists[pid]; ok { - perms = append(perms, p) - continue - } - np := Permission{ - PermissionId: pid, - Name: pid, // placeholder; your permission seeder will update - Description: "", - CreatedBy: rbacRole.CreatedBy, - } - if err := tx. - Where(&Permission{PermissionId: pid}). - Attrs(np). - FirstOrCreate(&np).Error; err != nil { - return fmt.Errorf("create missing permission %q: %w", pid, err) - } - perms = append(perms, np) - } - } - - // 3) Replace role -> permissions to match S3 exactly - // (idempotent; deletes any stale links, inserts new ones) - if err := tx.Model(&role).Association("Permissions").Replace(perms); err != nil { - return fmt.Errorf("set role permissions for %q: %w", rbacRole.ID, err) - } - - return nil - }) -} - - -func SeedUser(ctx context.Context, db *gorm.DB, rbacUser *rbac.UserAssignment) error { - return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { - // 1) Upsert user by unique Subject - u := User{ - Subject: rbacUser.Subject, - Email: rbacUser.Email, - CreatedAt: rbacUser.CreatedAt, // optional: trust S3 timestamps - UpdatedAt: rbacUser.UpdatedAt, - Version: rbacUser.Version, - } - - // If row exists (subject unique), update mutable fields - if err := tx.Clauses(clause.OnConflict{ - Columns: []clause.Column{{Name: "subject"}}, - DoUpdates: clause.AssignmentColumns([]string{"email", "updated_at", "version"}), - }).Create(&u).Error; err != nil { - return fmt.Errorf("upsert user %q: %w", rbacUser.Subject, err) - } - - // Ensure we have the actual row (ID may be needed for associations) - if err := tx.Where(&User{Subject: rbacUser.Subject}).First(&u).Error; err != nil { - return fmt.Errorf("load user %q: %w", rbacUser.Subject, err) - } - - // 2) Ensure all roles exist (by RoleId); create placeholders if missing - roles := make([]Role, 0, len(rbacUser.Roles)) - if len(rbacUser.Roles) > 0 { - var existing []Role - if err := tx.Where("role_id IN ?", rbacUser.Roles).Find(&existing).Error; err != nil { - return fmt.Errorf("lookup roles: %w", err) - } - byID := make(map[string]Role, len(existing)) - for _, r := range existing { - byID[r.RoleId] = r - } - for _, rid := range rbacUser.Roles { - if r, ok := byID[rid]; ok { - roles = append(roles, r) - continue - } - nr := Role{ - RoleId: rid, - Name: rid, // placeholder; your role seeder can update later - Description: "", - CreatedBy: rbacUser.Subject, - } - if err := tx.Where(&Role{RoleId: rid}).Attrs(nr).FirstOrCreate(&nr).Error; err != nil { - return fmt.Errorf("create missing role %q: %w", rid, err) - } - roles = append(roles, nr) - } - } - - // 3) Set user->roles to exactly match the S3 doc - if err := tx.Model(&u).Association("Roles").Replace(roles); err != nil { - return fmt.Errorf("set user roles for %q: %w", rbacUser.Subject, err) - } - - return nil - }) -} \ No newline at end of file diff --git a/taco/internal/db/queries.go b/taco/internal/db/queries.go deleted file mode 100644 index 05374cadf..000000000 --- a/taco/internal/db/queries.go +++ /dev/null @@ -1,90 +0,0 @@ -package db - -import ( - "gorm.io/gorm" - "log" -) - -func ListUnitsForUser(db *gorm.DB, userSubject string) ([]Unit, error) { - var units []Unit - - err := db.Where("id IN (?)", - db.Table("rule_units ru"). - Select("ru.unit_id"). - Joins("JOIN rules r ON ru.rule_id = r.id"). - Joins("JOIN role_permissions rp ON r.permission_id = rp.permission_id"). - Joins("JOIN user_roles ur ON rp.role_id = ur.role_id"). - Joins("JOIN users u ON ur.user_id = u.id"). - Where("u.subject = ? AND r.effect = 'allow'", userSubject)). - Preload("Tags"). - Find(&units).Error - - return units, err -} - - -// POC -// Replace S3Store.List -func ListAllUnits(db *gorm.DB, prefix string) ([]Unit, error) { - log.Println("ListAllUnits", prefix) - var units []Unit - query := db.Preload("Tags") - - if prefix != "" { - query = query.Where("name LIKE ?", prefix+"%") - } - - return units, query.Find(&units).Error -} - - - -// POC -func FilterUnitIDsByUser(db *gorm.DB, userSubject string, unitIDs []string) ([]string, error) { - log.Printf("FilterUnitIDsByUser: user=%s, checking %d units", userSubject, len(unitIDs)) - - if len(unitIDs) == 0 { - return []string{}, nil - } - - var allowedUnitIDs []string - - // Super simple query using the flattened view! - err := db.Table("user_unit_access"). - Select("unit_name"). - Where("user_subject = ?", userSubject). - Where("unit_name IN ?", unitIDs). - Pluck("unit_name", &allowedUnitIDs).Error - - log.Printf("User %s has access to %d/%d units", userSubject, len(allowedUnitIDs), len(unitIDs)) - return allowedUnitIDs, err -} - -func ListAllUnitsWithPrefix(db *gorm.DB, prefix string) ([]Unit, error) { - var units []Unit - query := db.Preload("Tags") - - if prefix != "" { - query = query.Where("name LIKE ?", prefix+"%") - } - - return units, query.Find(&units).Error -} - - - -/// POC - write to db example -// Sync functions to keep database in sync with storage operations -func SyncCreateUnit(db *gorm.DB, unitName string) error { - unit := Unit{Name: unitName} - return db.FirstOrCreate(&unit, Unit{Name: unitName}).Error -} - -func SyncDeleteUnit(db *gorm.DB, unitName string) error { - return db.Where("name = ?", unitName).Delete(&Unit{}).Error -} - -func SyncUnitExists(db *gorm.DB, unitName string) error { - unit := Unit{Name: unitName} - return db.FirstOrCreate(&unit, Unit{Name: unitName}).Error -} \ No newline at end of file diff --git a/taco/internal/middleware/auth.go b/taco/internal/middleware/auth.go index 92a11f948..caaef864a 100644 --- a/taco/internal/middleware/auth.go +++ b/taco/internal/middleware/auth.go @@ -74,11 +74,8 @@ func RBACMiddleware(rbacManager *rbac.RBACManager, signer *auth.Signer, action r func JWTAuthMiddleware(signer *auth.Signer) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { - log.Printf("DEBUG JWTAuthMiddleware: Called for path: %s", c.Request().URL.Path) - authz := c.Request().Header.Get("Authorization") if !strings.HasPrefix(authz, "Bearer ") { - log.Printf("DEBUG JWTAuthMiddleware: No Bearer token found") // No token, continue. The AuthorizingStore will block the request. return next(c) } @@ -86,13 +83,10 @@ func JWTAuthMiddleware(signer *auth.Signer) echo.MiddlewareFunc { token := strings.TrimSpace(strings.TrimPrefix(authz, "Bearer ")) claims, err := signer.VerifyAccess(token) if err != nil { - log.Printf("DEBUG JWTAuthMiddleware: Token verification failed: %v", err) // Invalid token, continue. The AuthorizingStore will block the request. return next(c) } - log.Printf("DEBUG JWTAuthMiddleware: Token verified for subject: %s", claims.Subject) - p := principal.Principal{ Subject: claims.Subject, Email: claims.Email, @@ -103,8 +97,6 @@ func JWTAuthMiddleware(signer *auth.Signer) echo.MiddlewareFunc { // Add the principal to the context for downstream stores and handlers. ctx := storage.ContextWithPrincipal(c.Request().Context(), p) c.SetRequest(c.Request().WithContext(ctx)) - - log.Printf("DEBUG JWTAuthMiddleware: Principal set in context for subject: %s", claims.Subject) return next(c) } diff --git a/taco/internal/query/common/sql_store.go b/taco/internal/query/common/sql_store.go index 1ad7b5306..f4fbd63e5 100644 --- a/taco/internal/query/common/sql_store.go +++ b/taco/internal/query/common/sql_store.go @@ -162,14 +162,7 @@ func (s *SQLStore) ListUnitsForUser(ctx context.Context, userSubject string, pre q = q.Where("units.name LIKE ?", prefix+"%") } - // DEBUG: Let's see what's being queried - log.Printf("DEBUG ListUnitsForUser: userSubject=%s, prefix=%s", userSubject, prefix) - - err := q.Find(&units).Error - - log.Printf("DEBUG ListUnitsForUser: found %d units, error: %v", len(units), err) - - return units, err + return units, q.Find(&units).Error } func (s *SQLStore) FilterUnitIDsByUser(ctx context.Context, userSubject string, unitIDs []string) ([]string, error) { diff --git a/taco/internal/storage/authorizer.go b/taco/internal/storage/authorizer.go index cc666cf60..1efeb7d2b 100644 --- a/taco/internal/storage/authorizer.go +++ b/taco/internal/storage/authorizer.go @@ -52,26 +52,17 @@ func NewAuthorizingStore(next UnitStore, qs query.Store) UnitStore { func (s *AuthorizingStore) List(ctx context.Context, prefix string) ([]*UnitMetadata, error) { principal, err := principalFromContext(ctx) if err != nil { - log.Printf("DEBUG AuthorizingStore.List: Failed to get principal from context: %v", err) return nil, errors.New("unauthorized") } - - log.Printf("DEBUG AuthorizingStore.List: Got principal: %+v", principal) // Use the optimized query that fetches ONLY the units the user is allowed to see. units, err := s.queryStore.ListUnitsForUser(ctx, principal.Subject, prefix) if err != nil { - log.Printf("DEBUG AuthorizingStore.List: ListUnitsForUser failed: %v", err) return nil, err } - - log.Printf("DEBUG AuthorizingStore.List: Found %d units for user %s", len(units), principal.Subject) - metadata := make([]*UnitMetadata, len(units)) for i, u := range units { - log.Printf("DEBUG: DB Unit: Name=%s, Size=%d, UpdatedAt=%v, Locked=%v", u.Name, u.Size, u.UpdatedAt, u.Locked) - var lockInfo *LockInfo if u.Locked { lockInfo = &LockInfo{ @@ -87,7 +78,6 @@ func (s *AuthorizingStore) List(ctx context.Context, prefix string) ([]*UnitMeta Locked: u.Locked, LockInfo: lockInfo, } - log.Printf("DEBUG: Mapped Metadata: ID=%s, Size=%d, Updated=%v", metadata[i].ID, metadata[i].Size, metadata[i].Updated) } return metadata, nil diff --git a/taco/internal/unit/handler.go b/taco/internal/unit/handler.go index e9f832378..ddc06fa26 100644 --- a/taco/internal/unit/handler.go +++ b/taco/internal/unit/handler.go @@ -404,65 +404,6 @@ func (h *Handler) GetUnitStatus(c echo.Context) error { } -// POC -// Add this new method to the existing Handler struct -// func (h *Handler) ListUnitsFast(c echo.Context) error { -// prefix := c.QueryParam("prefix") - -// // 1. Get all units from DATABASE -// allUnits, err := db.ListAllUnitsWithPrefix(h.db, prefix) -// if err != nil { -// return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to list units from database"}) -// } - -// // 2. Extract unit names and create map -// unitNames := make([]string, 0, len(allUnits)) -// unitMap := make(map[string]db.Unit) - -// for _, unit := range allUnits { -// unitNames = append(unitNames, unit.Name) -// unitMap[unit.Name] = unit -// } - -// // 3. RBAC filter with DATABASE -// if h.rbacManager != nil && h.signer != nil { -// principal, err := h.getPrincipalFromToken(c) -// if err != nil { -// if enabled, _ := h.rbacManager.IsEnabled(c.Request().Context()); enabled { -// return c.JSON(http.StatusUnauthorized, map[string]string{"error": "Failed to authenticate user"}) -// } -// } else { -// // RBAC filtering -// filteredNames, err := db.FilterUnitIDsByUser(h.db, principal.Subject, unitNames) -// if err != nil { -// return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to check permissions via database"}) -// } -// unitNames = filteredNames -// } -// } - -// // 4. Build response -// var responseUnits []*domain.Unit -// for _, name := range unitNames { -// if dbUnit, exists := unitMap[name]; exists { -// // Convert db.Unit to domain.Unit -// responseUnits = append(responseUnits, &domain.Unit{ -// ID: dbUnit.Name, -// Size: 0, // DB doesn't have size, could be calculated -// Updated: time.Now(), // Could add timestamp to db.Unit -// Locked: false, // Could check locks in database -// }) -// } -// } - -// domain.SortUnitsByID(responseUnits) -// return c.JSON(http.StatusOK, map[string]interface{}{ -// "units": responseUnits, -// "count": len(responseUnits), -// "source": "database", // POC identifier -// }) -// } - // Helpers func convertLockInfo(info *storage.LockInfo) *domain.Lock { From 9551793a102e31000f9ee9f97bb6bc0fa9e7611e Mon Sep 17 00:00:00 2001 From: Brian Reardon Date: Mon, 6 Oct 2025 19:50:20 -0700 Subject: [PATCH 08/13] remove noop store --- taco/internal/query/noop/store.go | 47 ------------------------------- 1 file changed, 47 deletions(-) delete mode 100644 taco/internal/query/noop/store.go diff --git a/taco/internal/query/noop/store.go b/taco/internal/query/noop/store.go deleted file mode 100644 index 82bc6008d..000000000 --- a/taco/internal/query/noop/store.go +++ /dev/null @@ -1,47 +0,0 @@ -package noop - -import ( - "context" - "errors" - - "github.com/diggerhq/digger/opentaco/internal/query/types" -) - -// NoOpQueryStore provides a disabled query backend that satisfies the Store interface. -type NoOpQueryStore struct{} - -func NewNoOpQueryStore() *NoOpQueryStore { - return &NoOpQueryStore{} -} - -func (n *NoOpQueryStore) Close() error { - return nil -} - -func (n *NoOpQueryStore) IsEnabled() bool { - return false -} - -var errDisabled = errors.New("query store is disabled") - -// UnitQuery implementation (no-op) -func (n *NoOpQueryStore) ListUnits(ctx context.Context, prefix string) ([]types.Unit, error) { - return nil, errDisabled -} -func (n *NoOpQueryStore) GetUnit(ctx context.Context, id string) (*types.Unit, error) { - return nil, errDisabled -} -func (n *NoOpQueryStore) SyncEnsureUnit(ctx context.Context, unitName string) error { - return errDisabled -} -func (n *NoOpQueryStore) SyncDeleteUnit(ctx context.Context, unitName string) error { - return errDisabled -} - -// RBACQuery implementation (no-op) -func (n *NoOpQueryStore) FilterUnitIDsByUser(ctx context.Context, userSubject string, unitIDs []string) ([]string, error) { - return nil, errDisabled -} -func (n *NoOpQueryStore) ListUnitsForUser(ctx context.Context, userSubject string, prefix string) ([]types.Unit, error) { - return nil, errDisabled -} From a9e0f3b48399992ec767a1abb0d1b1d809b55622 Mon Sep 17 00:00:00 2001 From: Brian Reardon Date: Mon, 6 Oct 2025 19:51:52 -0700 Subject: [PATCH 09/13] remove unit ls-fast test command --- taco/cmd/taco/commands/unit.go | 1 - 1 file changed, 1 deletion(-) diff --git a/taco/cmd/taco/commands/unit.go b/taco/cmd/taco/commands/unit.go index 1326523cc..b0995d200 100644 --- a/taco/cmd/taco/commands/unit.go +++ b/taco/cmd/taco/commands/unit.go @@ -44,7 +44,6 @@ func init() { unitCmd.AddCommand(unitVersionsCmd) unitCmd.AddCommand(unitRestoreCmd) unitCmd.AddCommand(unitStatusCmd) - unitCmd.AddCommand(unitLsFastCmd) } var unitCreateCmd = &cobra.Command{ From 72e6f6b21591a9dbf58dc2652ec7d702af48eeda Mon Sep 17 00:00:00 2001 From: Brian Reardon Date: Mon, 6 Oct 2025 19:56:08 -0700 Subject: [PATCH 10/13] remove units fast from the client --- taco/pkg/sdk/client.go | 34 ---------------------------------- 1 file changed, 34 deletions(-) diff --git a/taco/pkg/sdk/client.go b/taco/pkg/sdk/client.go index 12a8bf679..1e5936b2a 100644 --- a/taco/pkg/sdk/client.go +++ b/taco/pkg/sdk/client.go @@ -418,37 +418,3 @@ func (c *Client) Get(ctx context.Context, path string) (*http.Response, error) { func (c *Client) Delete(ctx context.Context, path string) (*http.Response, error) { return c.do(ctx, "DELETE", path, nil) } - - - - -type ListUnitsFastResponse struct { - Units []*UnitMetadata `json:"units"` - Count int `json:"count"` - Source string `json:"source"` -} - -// ListUnitsFast lists units using database (POC) -func (c *Client) ListUnitsFast(ctx context.Context, prefix string) (*ListUnitsFastResponse, error) { - path := "/v1/units-fast" - if prefix != "" { - path += "?prefix=" + url.QueryEscape(prefix) - } - - resp, err := c.do(ctx, "GET", path, nil) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, parseError(resp) - } - - var result ListUnitsFastResponse - if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { - return nil, fmt.Errorf("failed to decode response: %w", err) - } - - return &result, nil -} From 0370a6618bbdbcb61a8462dd2ab9045911cd4e8a Mon Sep 17 00:00:00 2001 From: Brian Reardon Date: Mon, 6 Oct 2025 19:58:24 -0700 Subject: [PATCH 11/13] remove WAL setup --- taco/configs/litestream.txt | 5 ----- taco/configs/litestream.yml | 6 ------ 2 files changed, 11 deletions(-) delete mode 100644 taco/configs/litestream.txt delete mode 100644 taco/configs/litestream.yml diff --git a/taco/configs/litestream.txt b/taco/configs/litestream.txt deleted file mode 100644 index ebae416ef..000000000 --- a/taco/configs/litestream.txt +++ /dev/null @@ -1,5 +0,0 @@ - -#restore command - -litestream restore -o /Users/brianreardon/development/digger/taco/data/taco.db \ - s3://open-taco-brian/backups/taco.db \ No newline at end of file diff --git a/taco/configs/litestream.yml b/taco/configs/litestream.yml deleted file mode 100644 index b92302af2..000000000 --- a/taco/configs/litestream.yml +++ /dev/null @@ -1,6 +0,0 @@ -dbs: - - path: /Users/brianreardon/development/digger/taco/data/taco.db - replicas: - - url: s3://open-taco-brian/backups/taco.db - region: us-east-2 - From 9a001511989c98aa9d430cecf1900ba2cc77c581 Mon Sep 17 00:00:00 2001 From: Brian Reardon Date: Mon, 6 Oct 2025 20:14:43 -0700 Subject: [PATCH 12/13] remove unnecessary --- taco/internal/domain/unit.go | 1 - taco/internal/middleware/auth.go | 1 - taco/internal/query/common/sql_store.go | 1 - taco/internal/storage/interface.go | 3 +- taco/internal/unit/handler.go | 68 +------------------------ 5 files changed, 2 insertions(+), 72 deletions(-) diff --git a/taco/internal/domain/unit.go b/taco/internal/domain/unit.go index a9b6267e7..7f7e26a2a 100644 --- a/taco/internal/domain/unit.go +++ b/taco/internal/domain/unit.go @@ -13,7 +13,6 @@ type Unit struct { Updated time.Time `json:"updated"` Locked bool `json:"locked"` LockInfo *Lock `json:"lock,omitempty"` - Tags []string `json:"tags,omitempty"` } // Lock represents lock information for a unit diff --git a/taco/internal/middleware/auth.go b/taco/internal/middleware/auth.go index caaef864a..c496f5686 100644 --- a/taco/internal/middleware/auth.go +++ b/taco/internal/middleware/auth.go @@ -3,7 +3,6 @@ package middleware import ( "net/http" "strings" - "log" "github.com/diggerhq/digger/opentaco/internal/auth" "github.com/diggerhq/digger/opentaco/internal/rbac" diff --git a/taco/internal/query/common/sql_store.go b/taco/internal/query/common/sql_store.go index f4fbd63e5..d700ad611 100644 --- a/taco/internal/query/common/sql_store.go +++ b/taco/internal/query/common/sql_store.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "log" "strings" "time" diff --git a/taco/internal/storage/interface.go b/taco/internal/storage/interface.go index 265839e41..6d64e4d58 100644 --- a/taco/internal/storage/interface.go +++ b/taco/internal/storage/interface.go @@ -55,10 +55,9 @@ type UnitStore interface { // Version operations ListVersions(ctx context.Context, id string) ([]*VersionInfo, error) RestoreVersion(ctx context.Context, id string, versionTimestamp time.Time, lockID string) error - } -// S3Store extends UnitStore with S3-specific accessors for integration +// S3Store extends UnitStore with S3-specific methods for RBAC integration type S3Store interface { UnitStore GetS3Client() *s3.Client diff --git a/taco/internal/unit/handler.go b/taco/internal/unit/handler.go index ddc06fa26..356f14054 100644 --- a/taco/internal/unit/handler.go +++ b/taco/internal/unit/handler.go @@ -16,7 +16,6 @@ import ( "github.com/google/uuid" "github.com/labstack/echo/v4" "log" - "context" ) @@ -77,8 +76,7 @@ func (h *Handler) ListUnits(c echo.Context) error { ctx := c.Request().Context() prefix := c.QueryParam("prefix") - // The RBAC logic is GONE. We just call the store. - // The store (AuthorizingStore) returns a pre-filtered list or an error. + unitsMetadata, err := h.store.List(ctx, prefix) if err != nil { if err.Error() == "unauthorized" || err.Error() == "forbidden" { @@ -107,65 +105,6 @@ func (h *Handler) ListUnits(c echo.Context) error { }) } -// listFromStorage encapsulates the old storage-based path (including RBAC). -func (h *Handler) listFromStorage(ctx context.Context, c echo.Context, prefix string) error { - items, err := h.store.List(ctx, prefix) - if err != nil { - return c.JSON(http.StatusInternalServerError, map[string]string{ - "error": "Failed to list units", - }) - } - - unitIDs := make([]string, 0, len(items)) - unitMap := make(map[string]*storage.UnitMetadata, len(items)) - for _, s := range items { - unitIDs = append(unitIDs, s.ID) - unitMap[s.ID] = s - } - - // Storage-based RBAC (manager-driven) - if h.rbacManager != nil && h.signer != nil { - principal, perr := h.getPrincipalFromToken(c) - if perr != nil { - if enabled, _ := h.rbacManager.IsEnabled(ctx); enabled { - return c.JSON(http.StatusUnauthorized, map[string]string{ - "error": "Failed to authenticate user", - }) - } - // RBAC not enabled -> show all units - } else { - filtered, ferr := h.rbacManager.FilterUnitsByReadAccess(ctx, principal, unitIDs) - if ferr != nil { - return c.JSON(http.StatusInternalServerError, map[string]string{ - "error": "Failed to check permissions", - }) - } - unitIDs = filtered - } - } - - // Build response - out := make([]*domain.Unit, 0, len(unitIDs)) - for _, id := range unitIDs { - if s, ok := unitMap[id]; ok { - out = append(out, &domain.Unit{ - ID: s.ID, - Size: s.Size, - Updated: s.Updated, - Locked: s.Locked, - LockInfo: convertLockInfo(s.LockInfo), - }) - } - } - domain.SortUnitsByID(out) - - return c.JSON(http.StatusOK, map[string]interface{}{ - "units": out, - "count": len(out), - }) -} - - func (h *Handler) GetUnit(c echo.Context) error { encodedID := c.Param("id") id := domain.DecodeUnitID(encodedID) @@ -198,8 +137,6 @@ func (h *Handler) DeleteUnit(c echo.Context) error { } return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to delete unit"}) } - - return c.NoContent(http.StatusNoContent) } @@ -249,7 +186,6 @@ func (h *Handler) UploadUnit(c echo.Context) error { } return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to upload unit"}) } - // Best-effort dependency graph update go deps.UpdateGraphOnWrite(c.Request().Context(), h.store, id, data) analytics.SendEssential("taco_unit_push_completed") @@ -403,8 +339,6 @@ func (h *Handler) GetUnitStatus(c echo.Context) error { return c.JSON(http.StatusOK, st) } - - // Helpers func convertLockInfo(info *storage.LockInfo) *domain.Lock { if info == nil { return nil } From 97555300aab4d160577e804e37f6b3634929a463 Mon Sep 17 00:00:00 2001 From: Brian Reardon Date: Mon, 6 Oct 2025 20:41:15 -0700 Subject: [PATCH 13/13] adjust auth --- taco/cmd/statesman/main.go | 8 +---- taco/internal/api/routes.go | 29 +++++---------- taco/internal/middleware/auth.go | 62 +++++++++++++------------------- 3 files changed, 34 insertions(+), 65 deletions(-) diff --git a/taco/cmd/statesman/main.go b/taco/cmd/statesman/main.go index 99b81135e..1ca7c24e9 100644 --- a/taco/cmd/statesman/main.go +++ b/taco/cmd/statesman/main.go @@ -22,7 +22,6 @@ import ( "github.com/diggerhq/digger/opentaco/internal/analytics" "github.com/diggerhq/digger/opentaco/internal/api" "github.com/diggerhq/digger/opentaco/internal/auth" - "github.com/diggerhq/digger/opentaco/internal/middleware" "github.com/diggerhq/digger/opentaco/internal/query" "github.com/diggerhq/digger/opentaco/internal/queryfactory" "github.com/diggerhq/digger/opentaco/internal/storage" @@ -180,17 +179,12 @@ func main() { e.Use(echomiddleware.CORS()) + // Create a signer for JWTs (this may need to be configured from env vars) signer, err := auth.NewSignerFromEnv() if err != nil { log.Fatalf("Failed to initialize JWT signer: %v", err) } - // Conditionally apply the authentication middleware. - if !*authDisable { - e.Use(middleware.JWTAuthMiddleware(signer)) - } - - // Pass the same signer instance to routes api.RegisterRoutes(e, finalStore, !*authDisable, queryStore, blobStore, signer) // Start server diff --git a/taco/internal/api/routes.go b/taco/internal/api/routes.go index 5fa848a81..0f3571b7e 100644 --- a/taco/internal/api/routes.go +++ b/taco/internal/api/routes.go @@ -112,25 +112,12 @@ func RegisterRoutes(e *echo.Echo, store storage.UnitStore, authEnabled bool, que // API v1 protected group v1 := e.Group("/v1") var verifyFn middleware.AccessTokenVerifier - if authEnabled { + if authEnabled && signer != nil { verifyFn = func(token string) error { - // JWT only for /v1 - if signer == nil { - return echo.ErrUnauthorized - } _, err := signer.VerifyAccess(token) - if err != nil { - // Debug: log the verification failure - fmt.Printf("[AUTH DEBUG] Token verification failed: %v\n", err) - tokenPreview := token - if len(token) > 50 { - tokenPreview = token[:50] + "..." - } - fmt.Printf("[AUTH DEBUG] Token preview: %s\n", tokenPreview) - } return err } - v1.Use(middleware.RequireAuth(verifyFn)) + v1.Use(middleware.RequireAuth(verifyFn, signer)) } // Setup RBAC manager if available (use underlyingStore for type assertion) @@ -180,20 +167,20 @@ func RegisterRoutes(e *echo.Echo, store storage.UnitStore, authEnabled bool, que } // Terraform HTTP backend proxy with RBAC middleware - backendHandler := backend.NewHandler(store) + backendHandler := backend.NewHandler(store) if authEnabled && rbacManager != nil { v1.GET("/backend/*", middleware.RBACMiddleware(rbacManager, signer, rbac.ActionUnitRead, "*")(backendHandler.GetState)) v1.POST("/backend/*", middleware.RBACMiddleware(rbacManager, signer, rbac.ActionUnitWrite, "*")(backendHandler.UpdateState)) v1.PUT("/backend/*", middleware.RBACMiddleware(rbacManager, signer, rbac.ActionUnitWrite, "*")(backendHandler.UpdateState)) // Explicitly wire non-standard HTTP methods used by Terraform backend - e.Add("LOCK", "/v1/backend/*", middleware.RequireAuth(verifyFn)(middleware.RBACMiddleware(rbacManager, signer, rbac.ActionUnitLock, "*")(backendHandler.HandleLockUnlock))) - e.Add("UNLOCK", "/v1/backend/*", middleware.RequireAuth(verifyFn)(middleware.RBACMiddleware(rbacManager, signer, rbac.ActionUnitLock, "*")(backendHandler.HandleLockUnlock))) + e.Add("LOCK", "/v1/backend/*", middleware.RequireAuth(verifyFn, signer)(middleware.RBACMiddleware(rbacManager, signer, rbac.ActionUnitLock, "*")(backendHandler.HandleLockUnlock))) + e.Add("UNLOCK", "/v1/backend/*", middleware.RequireAuth(verifyFn, signer)(middleware.RBACMiddleware(rbacManager, signer, rbac.ActionUnitLock, "*")(backendHandler.HandleLockUnlock))) } else if authEnabled { v1.GET("/backend/*", backendHandler.GetState) v1.POST("/backend/*", backendHandler.UpdateState) v1.PUT("/backend/*", backendHandler.UpdateState) - e.Add("LOCK", "/v1/backend/*", middleware.RequireAuth(verifyFn)(backendHandler.HandleLockUnlock)) - e.Add("UNLOCK", "/v1/backend/*", middleware.RequireAuth(verifyFn)(backendHandler.HandleLockUnlock)) + e.Add("LOCK", "/v1/backend/*", middleware.RequireAuth(verifyFn, signer)(backendHandler.HandleLockUnlock)) + e.Add("UNLOCK", "/v1/backend/*", middleware.RequireAuth(verifyFn, signer)(backendHandler.HandleLockUnlock)) } else { v1.GET("/backend/*", backendHandler.GetState) v1.POST("/backend/*", backendHandler.UpdateState) @@ -265,7 +252,7 @@ func RegisterRoutes(e *echo.Echo, store storage.UnitStore, authEnabled bool, que } return echo.ErrUnauthorized } - tfeGroup.Use(middleware.RequireAuth(tfeVerify)) + tfeGroup.Use(middleware.RequireAuth(tfeVerify, signer)) } // Move TFE endpoints to protected group diff --git a/taco/internal/middleware/auth.go b/taco/internal/middleware/auth.go index c496f5686..0cce1daff 100644 --- a/taco/internal/middleware/auth.go +++ b/taco/internal/middleware/auth.go @@ -15,8 +15,8 @@ import ( // It should return nil if valid, or an error if invalid. type AccessTokenVerifier func(token string) error -// RequireAuth returns middleware that verifies Bearer access tokens using the provided verifier. -func RequireAuth(verify AccessTokenVerifier) echo.MiddlewareFunc { +// RequireAuth returns middleware that verifies Bearer access tokens and sets principal in context. +func RequireAuth(verify AccessTokenVerifier, signer *auth.Signer) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { if verify == nil { @@ -27,9 +27,30 @@ func RequireAuth(verify AccessTokenVerifier) echo.MiddlewareFunc { return c.JSON(http.StatusUnauthorized, map[string]string{"error":"missing_bearer"}) } token := strings.TrimSpace(strings.TrimPrefix(authz, "Bearer ")) - if err := verify(token); err != nil { - return c.JSON(http.StatusUnauthorized, map[string]string{"error":"invalid_token"}) + + // Verify token and get claims in one call + if signer != nil { + claims, err := signer.VerifyAccess(token) + if err != nil { + return c.JSON(http.StatusUnauthorized, map[string]string{"error":"invalid_token"}) + } + + // Set principal in context + p := principal.Principal{ + Subject: claims.Subject, + Email: claims.Email, + Roles: claims.Roles, + Groups: claims.Groups, + } + ctx := storage.ContextWithPrincipal(c.Request().Context(), p) + c.SetRequest(c.Request().WithContext(ctx)) + } else { + // Fallback to generic verify function if no signer + if err := verify(token); err != nil { + return c.JSON(http.StatusUnauthorized, map[string]string{"error":"invalid_token"}) + } } + return next(c) } } @@ -69,39 +90,6 @@ func RBACMiddleware(rbacManager *rbac.RBACManager, signer *auth.Signer, action r } } -// JWTAuthMiddleware creates a middleware that verifies a JWT and injects the user principal into the request context. -func JWTAuthMiddleware(signer *auth.Signer) echo.MiddlewareFunc { - return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - authz := c.Request().Header.Get("Authorization") - if !strings.HasPrefix(authz, "Bearer ") { - // No token, continue. The AuthorizingStore will block the request. - return next(c) - } - - token := strings.TrimSpace(strings.TrimPrefix(authz, "Bearer ")) - claims, err := signer.VerifyAccess(token) - if err != nil { - // Invalid token, continue. The AuthorizingStore will block the request. - return next(c) - } - - p := principal.Principal{ - Subject: claims.Subject, - Email: claims.Email, - Roles: claims.Roles, - Groups: claims.Groups, - } - - // Add the principal to the context for downstream stores and handlers. - ctx := storage.ContextWithPrincipal(c.Request().Context(), p) - c.SetRequest(c.Request().WithContext(ctx)) - - return next(c) - } - } -} - // getPrincipalFromToken extracts principal information from the bearer token func getPrincipalFromToken(c echo.Context, signer *auth.Signer) (rbac.Principal, error) { authz := c.Request().Header.Get("Authorization")