Compare commits

...
Author SHA1 Message Date
Klaas Freitag 41a9ad809d Use inotifywait module from upstream 2026-06-09 15:39:21 +02:00
Ralf Haferkamp 0f97fc5e60 Merge pull request #2906 from opencloud-eu/fix/aarch64-idp-build
fix(idp): aarch64 build
2026-06-08 14:05:04 +02:00
Jannik Stehle f060ca4660 fix(idp): aarch64 build
Specifies the supported architectures to ensure that platform-specific
dependencies are fetched for every architecture we build/package for.
2026-06-08 11:11:14 +02:00
Ralf Haferkamp 3112d136ff Merge pull request #2883 from opencloud-eu/dependabot/npm_and_yarn/services/idp/axios-1.16.1
build(deps): bump axios from 1.16.0 to 1.16.1 in /services/idp
2026-06-08 09:27:52 +02:00
Ralf Haferkamp 4414bd0d08 Merge pull request #2879 from opencloud-eu/use-blessed-mapstructure-fork
switch to go-viper/mapstructure
2026-06-04 12:12:25 +02:00
Christian Richter 8593a1409a incorporate requested changes
Signed-off-by: Christian Richter <c.richter@opencloud.eu>
2026-06-03 16:28:44 +02:00
Christian Richter 6d9d2baa80 force ldap to listen only on localhost
Signed-off-by: Christian Richter <c.richter@opencloud.eu>
2026-06-03 16:28:44 +02:00
Christian Richter d6c6340705 use ldap instead of ldaps internally
Signed-off-by: Christian Richter <c.richter@opencloud.eu>
2026-06-03 16:28:44 +02:00
Ralf Haferkamp 9915fc8ea7 Merge pull request #2888 from dragonchaser/bump-reva
bump reva to 2.46.2
2026-06-03 11:49:28 +02:00
Christian Richter 6dff206e8a bump reva to 2.46.2
Signed-off-by: Christian Richter <c.richter@opencloud.eu>
2026-06-03 11:18:20 +02:00
dependabot[bot] a478f59c50 build(deps): bump axios from 1.16.0 to 1.16.1 in /services/idp
Bumps [axios](https://github.com/axios/axios) from 1.16.0 to 1.16.1.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.16.0...v1.16.1)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.16.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-02 19:57:24 +00:00
Jörn Friedrich Dreyer 48a9448ba7 switch to go-viper/mapstructure
Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
2026-06-02 13:57:37 +02:00
146 changed files with 1763 additions and 7849 deletions

No files matched your search

+6 -8
View File
@@ -33,8 +33,9 @@ require (
github.com/go-micro/plugins/v4/store/nats-js-kv v0.0.0-20240726082623-6831adfdcdc4
github.com/go-micro/plugins/v4/wrapper/monitoring/prometheus v1.2.0
github.com/go-micro/plugins/v4/wrapper/trace/opentelemetry v1.2.0
github.com/go-playground/validator/v10 v10.30.2
github.com/go-playground/validator/v10 v10.30.3
github.com/go-resty/resty/v2 v2.17.2
github.com/go-viper/mapstructure/v2 v2.5.0
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/golang/protobuf v1.5.4
github.com/google/go-cmp v0.7.0
@@ -52,7 +53,6 @@ require (
github.com/leonelquinteros/gotext v1.7.3-0.20260422134830-b012b4ccae69
github.com/libregraph/idm v0.5.0
github.com/libregraph/lico v0.66.0
github.com/mitchellh/mapstructure v1.5.0
github.com/mna/pigeon v1.3.0
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
github.com/nats-io/nats-server/v2 v2.14.0
@@ -64,7 +64,7 @@ require (
github.com/open-policy-agent/opa v1.15.2
github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260310090739-853d972b282d
github.com/opencloud-eu/reva/v2 v2.46.2-0.20260602061404-0e975e5456eb
github.com/opencloud-eu/reva/v2 v2.46.2
github.com/opensearch-project/opensearch-go/v4 v4.6.0
github.com/orcaman/concurrent-map v1.0.0
github.com/pkg/errors v0.9.1
@@ -102,7 +102,7 @@ require (
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0
go.opentelemetry.io/otel/sdk v1.44.0
go.opentelemetry.io/otel/trace v1.44.0
golang.org/x/crypto v0.51.0
golang.org/x/crypto v0.52.0
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f
golang.org/x/image v0.40.0
golang.org/x/net v0.55.0
@@ -223,7 +223,6 @@ require (
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/go-test/deep v1.1.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
@@ -315,7 +314,7 @@ require (
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
github.com/pablodz/inotifywaitgo v0.0.9 // indirect
github.com/pablodz/inotifywaitgo v0.0.12 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
@@ -413,5 +412,4 @@ exclude github.com/mattn/go-sqlite3 v2.0.3+incompatible
replace github.com/go-micro/plugins/v4/store/nats-js-kv => github.com/opencloud-eu/go-micro-plugins/v4/store/nats-js-kv v0.0.0-20250512152754-23325793059a
// to get the logger injection (https://github.com/pablodz/inotifywaitgo/pull/11)
replace github.com/pablodz/inotifywaitgo v0.0.9 => github.com/opencloud-eu/inotifywaitgo v0.0.0-20251111171128-a390bae3c5e9
replace github.com/opencloud-eu/reva/v2 => github.com/opencloud-eu/reva/v2 v2.46.3-0.20260609111154-079ecc53abaa
+10 -12
View File
@@ -449,8 +449,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ=
github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc=
github.com/go-playground/validator/v10 v10.30.3 h1:4MU6YkEwx7GbcPJOZxrtbu+QfF3pJLJuaYTeAH0DYy8=
github.com/go-playground/validator/v10 v10.30.3/go.mod h1:4Axh7oCNGcoGkqLoE4YWt6n20mcEIsPRlB7vPk3lpyc=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48/go.mod h1:dZGr0i9PLlaaTD4H/hoZIDjQ+r6xq8mgbRzHZf7f2J8=
@@ -466,8 +466,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVruqWQJBtW6+bTBDTniY8yZum5rF3b5jw=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
@@ -857,8 +857,6 @@ github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0Qu
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mna/pigeon v1.3.0 h1:/3fzVrl1C2RK3x04tyL+ribn+3S3VSEFFbCFLmRPAoc=
@@ -948,12 +946,10 @@ github.com/opencloud-eu/go-micro-plugins/v4/store/nats-js-kv v0.0.0-202505121527
github.com/opencloud-eu/go-micro-plugins/v4/store/nats-js-kv v0.0.0-20250512152754-23325793059a/go.mod h1:pjcozWijkNPbEtX5SIQaxEW/h8VAVZYTLx+70bmB3LY=
github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89 h1:W1ms+lP5lUUIzjRGDg93WrQfZJZCaV1ZP3KeyXi8bzY=
github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89/go.mod h1:vigJkNss1N2QEceCuNw/ullDehncuJNFB6mEnzfq9UI=
github.com/opencloud-eu/inotifywaitgo v0.0.0-20251111171128-a390bae3c5e9 h1:dIftlX03Bzfbujhp9B54FbgER0VBDWJi/w8RBxJlzxU=
github.com/opencloud-eu/inotifywaitgo v0.0.0-20251111171128-a390bae3c5e9/go.mod h1:JWyDC6H+5oZRdUJUgKuaye+8Ph5hEs6HVzVoPKzWSGI=
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260310090739-853d972b282d h1:JcqGDiyrcaQwVyV861TUyQgO7uEmsjkhfm7aQd84dOw=
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260310090739-853d972b282d/go.mod h1:pzatilMEHZFT3qV7C/X3MqOa3NlRQuYhlRhZTL+hN6Q=
github.com/opencloud-eu/reva/v2 v2.46.2-0.20260602061404-0e975e5456eb h1:7LXIqaeVQqOghfTw44kyLSak+cR0TIBl55M4+Tb08f8=
github.com/opencloud-eu/reva/v2 v2.46.2-0.20260602061404-0e975e5456eb/go.mod h1:V1bwCQXM1pGmswtjdSJVIweA9Vv5EmtUZr/7Cm5086A=
github.com/opencloud-eu/reva/v2 v2.46.3-0.20260609111154-079ecc53abaa h1:MSS7EwbPh74IpH0Tidn6JoeMNF1D1A66MaWk0Ugqnv0=
github.com/opencloud-eu/reva/v2 v2.46.3-0.20260609111154-079ecc53abaa/go.mod h1:RoFQt+u7edxwzHr1IZ2Y6VaDinMiRPQupAvMBy3WVmE=
github.com/opencloud-eu/secure v0.0.0-20260312082735-b6f5cb2244e4 h1:l2oB/RctH+t8r7QBj5p8thfEHCM/jF35aAY3WQ3hADI=
github.com/opencloud-eu/secure v0.0.0-20260312082735-b6f5cb2244e4/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
@@ -972,6 +968,8 @@ github.com/orcaman/concurrent-map v1.0.0/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CF
github.com/ovh/go-ovh v1.1.0/go.mod h1:AxitLZ5HBRPyUd+Zl60Ajaag+rNTdVXWIkzfrVuTXWA=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
github.com/pablodz/inotifywaitgo v0.0.12 h1:DVJJTFMDlSAdO46yisPF2vzozs/r+4k6ih8MVghq78M=
github.com/pablodz/inotifywaitgo v0.0.12/go.mod h1:JWyDC6H+5oZRdUJUgKuaye+8Ph5hEs6HVzVoPKzWSGI=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
@@ -1362,8 +1360,8 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
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.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -1,7 +1,7 @@
package revaconfig
import (
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/services/app-registry/pkg/config"
)
@@ -1,9 +1,6 @@
package defaults
import (
"path/filepath"
"github.com/opencloud-eu/opencloud/pkg/config/defaults"
"github.com/opencloud-eu/opencloud/pkg/shared"
"github.com/opencloud-eu/opencloud/pkg/structs"
"github.com/opencloud-eu/opencloud/services/auth-basic/pkg/config"
@@ -38,8 +35,7 @@ func DefaultConfig() *config.Config {
AuthProvider: "ldap",
AuthProviders: config.AuthProviders{
LDAP: config.LDAPProvider{
URI: "ldaps://localhost:9235",
CACert: filepath.Join(defaults.BaseDataPath(), "idm", "ldap.crt"),
URI: "ldap://localhost:9236",
Insecure: false,
UserBaseDN: "ou=users,o=libregraph-idm",
GroupBaseDN: "ou=groups,o=libregraph-idm",
@@ -1,11 +1,9 @@
package defaults
import (
"path"
"strings"
"time"
"github.com/opencloud-eu/opencloud/pkg/config/defaults"
"github.com/opencloud-eu/opencloud/pkg/shared"
"github.com/opencloud-eu/opencloud/pkg/structs"
"github.com/opencloud-eu/opencloud/services/graph/pkg/config"
@@ -79,9 +77,8 @@ func DefaultConfig() *config.Config {
Identity: config.Identity{
Backend: "ldap",
LDAP: config.LDAP{
URI: "ldaps://localhost:9235",
URI: "ldap://localhost:9236",
Insecure: false,
CACert: path.Join(defaults.BaseDataPath(), "idm", "ldap.crt"),
BindDN: "uid=libregraph,ou=sysusers,o=libregraph-idm",
UseServerUUID: false,
UsePasswordModExOp: true,
@@ -1,9 +1,6 @@
package defaults
import (
"path/filepath"
"github.com/opencloud-eu/opencloud/pkg/config/defaults"
"github.com/opencloud-eu/opencloud/pkg/shared"
"github.com/opencloud-eu/opencloud/pkg/structs"
"github.com/opencloud-eu/opencloud/services/groups/pkg/config"
@@ -38,8 +35,7 @@ func DefaultConfig() *config.Config {
Driver: "ldap",
Drivers: config.Drivers{
LDAP: config.LDAPDriver{
URI: "ldaps://localhost:9235",
CACert: filepath.Join(defaults.BaseDataPath(), "idm", "ldap.crt"),
URI: "ldap://localhost:9236",
Insecure: false,
UserBaseDN: "ou=users,o=libregraph-idm",
GroupBaseDN: "ou=groups,o=libregraph-idm",
+15 -12
View File
@@ -8,10 +8,11 @@ import (
"html/template"
"os"
"os/signal"
"path"
"strings"
"github.com/opencloud-eu/opencloud/pkg/config/configlog"
pkgcrypto "github.com/opencloud-eu/opencloud/pkg/crypto"
"github.com/opencloud-eu/opencloud/pkg/config/defaults"
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/pkg/runner"
"github.com/opencloud-eu/opencloud/services/idm"
@@ -47,23 +48,25 @@ func Server(cfg *config.Config) *cobra.Command {
gr := runner.NewGroup()
{
servercfg := server.Config{
Logger: log.LogrusWrap(logger.Logger),
LDAPHandler: "boltdb",
LDAPSListenAddr: cfg.IDM.LDAPSAddr,
TLSCertFile: cfg.IDM.Cert,
TLSKeyFile: cfg.IDM.Key,
LDAPBaseDN: "o=libregraph-idm",
LDAPAdminDN: "uid=libregraph,ou=sysusers,o=libregraph-idm",
Logger: log.LogrusWrap(logger.Logger),
LDAPHandler: "boltdb",
LDAPListenAddr: cfg.IDM.LDAPAddr,
LDAPBaseDN: "o=libregraph-idm",
LDAPAdminDN: "uid=libregraph,ou=sysusers,o=libregraph-idm",
BoltDBFile: cfg.IDM.DatabasePath,
}
if cfg.IDM.LDAPSAddr != "" {
// Generate a self-signing cert if no certificate is present
if err := pkgcrypto.GenCert(cfg.IDM.Cert, cfg.IDM.Key, logger); err != nil {
logger.Fatal().Err(err).Msgf("Could not generate test-certificate")
}
servercfg.LDAPSListenAddr = cfg.IDM.LDAPSAddr
servercfg.TLSCertFile = cfg.IDM.Cert
servercfg.TLSKeyFile = cfg.IDM.Key
}
if err := os.MkdirAll(path.Join(defaults.BaseDataPath(), "idm"), 0700); err != nil {
logger.Fatal().Err(err).Msgf("Could not create data directory for idm")
}
if _, err := os.Stat(servercfg.BoltDBFile); errors.Is(err, os.ErrNotExist) {
logger.Debug().Msg("Bootstrapping IDM database")
if err = bootstrap(logger, cfg, servercfg); err != nil {
+1
View File
@@ -27,6 +27,7 @@ type Config struct {
type Settings struct {
LDAPSAddr string `yaml:"ldaps_addr" env:"IDM_LDAPS_ADDR" desc:"Listen address for the LDAPS listener (ip-addr:port)." introductionVersion:"1.0.0"`
LDAPAddr string
Cert string `yaml:"cert" env:"IDM_LDAPS_CERT" desc:"File name of the TLS server certificate for the LDAPS listener. If not defined, the root directory derives from $OC_BASE_DATA_PATH/idm." introductionVersion:"1.0.0"`
Key string `yaml:"key" env:"IDM_LDAPS_KEY" desc:"File name for the TLS certificate key for the server certificate. If not defined, the root directory derives from $OC_BASE_DATA_PATH/idm." introductionVersion:"1.0.0"`
DatabasePath string `yaml:"database" env:"IDM_DATABASE_PATH" desc:"Full path to the IDM backend database. If not defined, the root directory derives from $OC_BASE_DATA_PATH/idm." introductionVersion:"1.0.0"`
@@ -30,9 +30,7 @@ func DefaultConfig() *config.Config {
CreateDemoUsers: false,
DemoUsersIssuerUrl: "https://localhost:9200",
IDM: config.Settings{
LDAPSAddr: "127.0.0.1:9235",
Cert: path.Join(defaults.BaseDataPath(), "idm", "ldap.crt"),
Key: path.Join(defaults.BaseDataPath(), "idm", "ldap.key"),
LDAPAddr: "127.0.0.1:9236",
DatabasePath: path.Join(defaults.BaseDataPath(), "idm", "idm.boltdb"),
},
}
@@ -51,5 +49,5 @@ func EnsureDefaults(cfg *config.Config) {
// Sanitize sanitizes the configuration
func Sanitize(cfg *config.Config) {
// nothing to sanitize here
// nothing to do yet
}
+21
View File
@@ -2,6 +2,7 @@ package parser
import (
"errors"
"net"
occfg "github.com/opencloud-eu/opencloud/pkg/config"
"github.com/opencloud-eu/opencloud/pkg/shared"
@@ -53,5 +54,25 @@ func Validate(cfg *config.Config) error {
return shared.MissingServiceUserPassword(cfg.Service.Name, "REVA")
}
ip, err := net.ResolveTCPAddr("tcp", cfg.IDM.LDAPAddr) // validate the LDAP address if set
if err != nil {
return errors.New("invalid configuration: 'ldap_addr' is not a valid address")
}
if !ip.IP.IsLoopback() {
// loopback addresses are allowed to be used with ldap_addr, but not with ldaps_addr, for security reasons
return errors.New("invalid configuration: 'ldap_addr' is set but 'ldaps_addr' is not set. For security reasons, the 'ldap_addr' setting is only allowed to be used with loopback addresses. Please set 'ldaps_addr' to a valid address and port to listen for LDAPS connections")
}
if cfg.IDM.LDAPSAddr != "" {
if cfg.IDM.Cert == "" {
return errors.New("invalid configuration: 'ldaps_addr' is set but 'cert' is not set. Please set 'cert' to a valid path to a TLS certificate")
}
if cfg.IDM.Key == "" {
return errors.New("invalid configuration: 'ldaps_addr' is set but 'key' is not set. Please set 'key' to a valid path to a TLS certificate key")
}
}
return nil
}
+1 -1
View File
@@ -35,7 +35,7 @@
"@types/react-dom": "^17.0.26",
"@types/react-redux": "^7.1.34",
"@types/redux-logger": "^3.0.13",
"axios": "^1.16.0",
"axios": "^1.16.1",
"i18next": "^26.3.0",
"i18next-browser-languagedetector": "^8.2.1",
"i18next-resources-to-backend": "^1.2.1",
@@ -119,8 +119,7 @@ func DefaultConfig() *config.Config {
},
},
Ldap: config.Ldap{
URI: "ldaps://localhost:9235",
TLSCACert: filepath.Join(defaults.BaseDataPath(), "idm", "ldap.crt"),
URI: "ldap://localhost:9236",
BindDN: "uid=idp,ou=sysusers,o=libregraph-idm",
BaseDN: "ou=users,o=libregraph-idm",
Scope: "sub",
+50 -19
View File
@@ -34,8 +34,8 @@ importers:
specifier: ^3.0.13
version: 3.0.13
axios:
specifier: ^1.16.0
version: 1.16.0
specifier: ^1.16.1
version: 1.16.1
i18next:
specifier: ^26.3.0
version: 26.3.0(typescript@6.0.3)
@@ -1882,6 +1882,10 @@ packages:
resolution: {integrity: sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==}
engines: {node: '>=8.9'}
agent-base@6.0.2:
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
engines: {node: '>= 6.0.0'}
agent-base@7.1.4:
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
engines: {node: '>= 14'}
@@ -1978,8 +1982,8 @@ packages:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'}
axios@1.16.0:
resolution: {integrity: sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==}
axios@1.16.1:
resolution: {integrity: sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==}
b4a@1.8.0:
resolution: {integrity: sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==}
@@ -2943,6 +2947,10 @@ packages:
resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==}
engines: {node: '>= 0.4'}
hasown@2.0.4:
resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==}
engines: {node: '>= 0.4'}
he@1.2.0:
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
hasBin: true
@@ -2996,6 +3004,10 @@ packages:
resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
engines: {node: '>= 14'}
https-proxy-agent@5.0.1:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'}
https-proxy-agent@7.0.6:
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
engines: {node: '>= 14'}
@@ -7112,6 +7124,12 @@ snapshots:
loader-utils: 2.0.4
regex-parser: 2.3.1
agent-base@6.0.2:
dependencies:
debug: 4.4.3
transitivePeerDependencies:
- supports-color
agent-base@7.1.4: {}
ajv-formats@2.1.1(ajv@8.20.0):
@@ -7200,13 +7218,15 @@ snapshots:
dependencies:
possible-typed-array-names: 1.1.0
axios@1.16.0:
axios@1.16.1:
dependencies:
follow-redirects: 1.16.0
form-data: 4.0.5
https-proxy-agent: 5.0.1
proxy-from-env: 2.1.0
transitivePeerDependencies:
- debug
- supports-color
b4a@1.8.0: {}
@@ -7894,7 +7914,7 @@ snapshots:
has-property-descriptors: 1.0.2
has-proto: 1.2.0
has-symbols: 1.1.0
hasown: 2.0.3
hasown: 2.0.4
internal-slot: 1.1.0
is-array-buffer: 3.0.5
is-callable: 1.2.7
@@ -7946,7 +7966,7 @@ snapshots:
es-errors: 1.3.0
get-intrinsic: 1.3.0
has-tostringtag: 1.0.2
hasown: 2.0.3
hasown: 2.0.4
es-to-primitive@1.3.0:
dependencies:
@@ -8109,7 +8129,7 @@ snapshots:
asynckit: 0.4.0
combined-stream: 1.0.8
es-set-tostringtag: 2.1.0
hasown: 2.0.3
hasown: 2.0.4
mime-types: 2.1.35
fraction.js@5.3.4: {}
@@ -8180,7 +8200,7 @@ snapshots:
call-bound: 1.0.4
define-properties: 1.2.1
functions-have-names: 1.2.3
hasown: 2.0.3
hasown: 2.0.4
is-callable: 1.2.7
functions-have-names@1.2.3: {}
@@ -8196,12 +8216,12 @@ snapshots:
call-bind-apply-helpers: 1.0.2
es-define-property: 1.0.1
es-errors: 1.3.0
es-object-atoms: 1.1.1
es-object-atoms: 1.1.2
function-bind: 1.1.2
get-proto: 1.0.1
gopd: 1.2.0
has-symbols: 1.1.0
hasown: 2.0.3
hasown: 2.0.4
math-intrinsics: 1.1.0
get-own-enumerable-property-symbols@3.0.2: {}
@@ -8209,7 +8229,7 @@ snapshots:
get-proto@1.0.1:
dependencies:
dunder-proto: 1.0.1
es-object-atoms: 1.1.1
es-object-atoms: 1.1.2
get-symbol-description@1.1.0:
dependencies:
@@ -8349,6 +8369,10 @@ snapshots:
dependencies:
function-bind: 1.1.2
hasown@2.0.4:
dependencies:
function-bind: 1.1.2
he@1.2.0: {}
heimdalljs-logger@0.1.10:
@@ -8426,6 +8450,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
https-proxy-agent@5.0.1:
dependencies:
agent-base: 6.0.2
debug: 4.4.3
transitivePeerDependencies:
- supports-color
https-proxy-agent@7.0.6:
dependencies:
agent-base: 7.1.4
@@ -8530,7 +8561,7 @@ snapshots:
internal-slot@1.1.0:
dependencies:
es-errors: 1.3.0
hasown: 2.0.3
hasown: 2.0.4
side-channel: 1.1.0
ip-address@10.2.0: {}
@@ -8629,7 +8660,7 @@ snapshots:
call-bound: 1.0.4
gopd: 1.2.0
has-tostringtag: 1.0.2
hasown: 2.0.3
hasown: 2.0.4
is-regexp@1.0.0: {}
@@ -9120,7 +9151,7 @@ snapshots:
call-bind: 1.0.9
call-bound: 1.0.4
define-properties: 1.2.1
es-object-atoms: 1.1.1
es-object-atoms: 1.1.2
has-symbols: 1.1.0
object-keys: 1.1.1
@@ -10165,7 +10196,7 @@ snapshots:
dependencies:
dunder-proto: 1.0.1
es-errors: 1.3.0
es-object-atoms: 1.1.1
es-object-atoms: 1.1.2
shebang-command@2.0.0:
dependencies:
@@ -10370,7 +10401,7 @@ snapshots:
define-data-property: 1.1.4
define-properties: 1.2.1
es-abstract: 1.24.2
es-object-atoms: 1.1.1
es-object-atoms: 1.1.2
has-property-descriptors: 1.0.2
string.prototype.trimend@1.0.9:
@@ -10378,13 +10409,13 @@ snapshots:
call-bind: 1.0.9
call-bound: 1.0.4
define-properties: 1.2.1
es-object-atoms: 1.1.1
es-object-atoms: 1.1.2
string.prototype.trimstart@1.0.8:
dependencies:
call-bind: 1.0.9
define-properties: 1.2.1
es-object-atoms: 1.1.1
es-object-atoms: 1.1.2
string_decoder@1.1.1:
dependencies:
+16
View File
@@ -3,6 +3,22 @@ autoInstallPeers: true
publicHoistPattern:
- "*"
# Ensure platform-specific optional dependencies (e.g. Rollup's native
# binaries pulled in via workbox) are fetched for every architecture we
# build/package for, not just the host that runs `pnpm install`. Without
# this, cross-arch RPM/OBS builds fail with "Cannot find module
# @rollup/rollup-linux-arm64-gnu" on aarch64.
supportedArchitectures:
os:
- current
- linux
cpu:
- x64
- arm64
libc:
- glibc
- musl
overrides:
fast-uri: ">=3.1.2"
postcss: ">=8.5.10"
@@ -1,9 +1,6 @@
package defaults
import (
"path/filepath"
"github.com/opencloud-eu/opencloud/pkg/config/defaults"
"github.com/opencloud-eu/opencloud/pkg/shared"
"github.com/opencloud-eu/opencloud/pkg/structs"
"github.com/opencloud-eu/opencloud/services/users/pkg/config"
@@ -38,8 +35,7 @@ func DefaultConfig() *config.Config {
Driver: "ldap",
Drivers: config.Drivers{
LDAP: config.LDAPDriver{
URI: "ldaps://localhost:9235",
CACert: filepath.Join(defaults.BaseDataPath(), "idm", "ldap.crt"),
URI: "ldap://localhost:9236",
Insecure: false,
UserBaseDN: "ou=users,o=libregraph-idm",
GroupBaseDN: "ou=groups,o=libregraph-idm",
@@ -30,3 +30,4 @@ _testmain.go
cover.html
README.html
.idea
.claude/
+5
View File
@@ -53,3 +53,8 @@ linters:
- varnamelen
- wrapcheck
- wsl
- gomodguard
settings:
govet:
disable:
- inline
+7 -2
View File
@@ -51,7 +51,8 @@ Validator returns only InvalidValidationError for bad validation input, nil or V
```go
err := validate.Struct(mystruct)
validationErrors := err.(validator.ValidationErrors)
var validationErrors validator.ValidationErrors
errors.As(err, &validationErrors)
```
Usage and documentation
@@ -128,6 +129,7 @@ validate := validator.New(validator.WithRequiredStructEnabled())
| url | URL String |
| http_url | HTTP(s) URL String |
| https_url | HTTPS-only URL String |
| origin | Web origin (URL with HTTP(S) scheme and host, but no path/query/fragment) |
| url_encoded | URL Encoded |
| urn_rfc2141 | Urn RFC 2141 String |
@@ -169,6 +171,7 @@ validate := validator.New(validator.WithRequiredStructEnabled())
| bic_iso_9362_2014 | Business Identifier Code (ISO 9362:2014) |
| bic | Business Identifier Code (ISO 9362:2022) |
| bcp47_language_tag | Language tag (BCP 47) |
| bcp47_strict_language_tag | Language tag (BCP 47), strictly following RFC 5646 |
| btc_addr | Bitcoin Address |
| btc_addr_bech32 | Bitcoin Bech32 Address (segwit) |
| credit_card | Credit Card Number |
@@ -222,7 +225,7 @@ validate := validator.New(validator.WithRequiredStructEnabled())
| sha384 | SHA384 hash |
| sha512 | SHA512 hash |
| ripemd128 | RIPEMD-128 hash |
| ripemd128 | RIPEMD-160 hash |
| ripemd160 | RIPEMD-160 hash |
| tiger128 | TIGER128 hash |
| tiger160 | TIGER160 hash |
| tiger192 | TIGER192 hash |
@@ -250,11 +253,13 @@ validate := validator.New(validator.WithRequiredStructEnabled())
| file | Existing File |
| filepath | File Path |
| image | Image |
| mimetype | MIME Type |
| isdefault | Is Default |
| len | Length |
| max | Maximum |
| min | Minimum |
| oneof | One Of |
| noneof | None Of |
| required | Required |
| required_if | Required If |
| required_unless | Required Unless |
+314 -94
View File
@@ -3,7 +3,6 @@ package validator
import (
"bufio"
"bytes"
"cmp"
"context"
"crypto/sha256"
"encoding/hex"
@@ -16,7 +15,9 @@ import (
"net/url"
"os"
"reflect"
"regexp"
"runtime"
"slices"
"strconv"
"strings"
"sync"
@@ -141,6 +142,7 @@ var (
"http_url": isHttpURL,
"https_url": isHttpsURL,
"uri": isURI,
"origin": isOrigin,
"urn_rfc2141": isUrnRFC2141, // RFC 2141
"file": isFile,
"filepath": isFilePath,
@@ -159,6 +161,7 @@ var (
"startsnotwith": startsNotWith,
"endsnotwith": endsNotWith,
"image": isImage,
"mimetype": isMIMEType,
"isbn": isISBN,
"isbn10": isISBN10,
"isbn13": isISBN13,
@@ -217,6 +220,8 @@ var (
"unique": isUnique,
"oneof": isOneOf,
"oneofci": isOneOfCI,
"noneof": isNoneOf,
"noneofci": isNoneOfCI,
"html": isHTML,
"html_encoded": isHTMLEncoded,
"url_encoded": isURLEncoded,
@@ -240,6 +245,7 @@ var (
"iso4217": isIso4217,
"iso4217_numeric": isIso4217Numeric,
"bcp47_language_tag": isBCP47LanguageTag,
"bcp47_strict_language_tag": isBCP47StrictLanguageTag,
"postcode_iso3166_alpha2": isPostcodeByIso3166Alpha2,
"postcode_iso3166_alpha2_field": isPostcodeByIso3166Alpha2Field,
"bic_iso_9362_2014": isIsoBic2014Format,
@@ -261,6 +267,33 @@ var (
var (
oneofValsCache = map[string][]string{}
oneofValsCacheRWLock = sync.RWMutex{}
// BCP47 language tag
// according to https://www.rfc-editor.org/rfc/bcp/bcp47.txt
bcp47LanguageTagRe = regexp.MustCompile(strings.Join([]string{
// group 1:
`^(`,
// irregular
`en-gb-oed|i-ami|i-bnn|i-default|i-enochian|i-hak|i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|i-tay|i-tsu|`,
`sgn-be-fr|sgn-be-nl|sgn-ch-de|`,
// regular
`art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang|`,
// privateuse
`x-[a-z0-9]{1,8}`,
`)$`,
`|`,
// langtag
`^`,
`((?:[a-z]{2,3}(?:-[a-z]{3}){0,3})|[a-z]{4}|[a-z]{5,8})`, // group 2: language
`(?:-([a-z]{4}))?`, // group 3: script
`(?:-([a-z]{2}|[0-9]{3}))?`, // group 4: region
`(?:-((?:[a-z0-9]{5,8}|[0-9][a-z0-9]{3})(?:-(?:[a-z0-9]{5,8}|[0-9][a-z0-9]{3}))*))?`, // group 5: variant
`(?:-((?:[a-wyz0-9](?:-[a-z0-9]{2,8})+)(?:-(?:[a-wyz0-9](?:-[a-z0-9]{2,8})+))*))?`, // group 6: extension
`(?:-x(?:-[a-z0-9]{1,8})+)?`,
`$`,
}, ""))
)
func parseOneOfParam2(s string) []string {
@@ -307,12 +340,8 @@ func isOneOf(fl FieldLevel) bool {
default:
panic(fmt.Sprintf("Bad field type %s", field.Type()))
}
for i := 0; i < len(vals); i++ {
if vals[i] == v {
return true
}
}
return false
return slices.Contains(vals, v)
}
// isOneOfCI is the validation function for validating if the current field's value is one of the provided string values (case insensitive).
@@ -323,13 +352,20 @@ func isOneOfCI(fl FieldLevel) bool {
if field.Kind() != reflect.String {
panic(fmt.Sprintf("Bad field type %s", field.Type()))
}
v := field.String()
for _, val := range vals {
if strings.EqualFold(val, v) {
return true
}
}
return false
return slices.ContainsFunc(vals, func(val string) bool {
return strings.EqualFold(val, field.String())
})
}
// isNoneOf validates that the current field's value is not one of the provided string or integer values
func isNoneOf(fl FieldLevel) bool {
return !isOneOf(fl)
}
// isNoneOfCI validates that the current field's value is not one of the provided string values (case insensitive)
func isNoneOfCI(fl FieldLevel) bool {
return !isOneOfCI(fl)
}
// isUnique is the validation function for validating if each array|slice|map value is unique
@@ -1526,6 +1562,64 @@ func isURI(fl FieldLevel) bool {
panic(fmt.Sprintf("Bad field type %s", field.Type()))
}
// isOrigin checks if a field value is a valid web origin URL with HTTP(S) scheme and host defined, but no path, query, or fragment.
func isOrigin(fl FieldLevel) bool {
field := fl.Field()
if field.Kind() == reflect.String {
s := field.String()
if len(s) == 0 {
return false
}
// Fragments with empty content ("#") are not detectable after parse and
// u.Fragment will be empty even if # is present in the URL.
if strings.Contains(s, "#") {
return false
}
u, err := url.Parse(s)
if err != nil {
return false
}
if u.Scheme != "http" && u.Scheme != "https" {
return false
}
if u.Path != "" || u.RawQuery != "" || u.ForceQuery || u.Fragment != "" {
return false
}
if u.User != nil {
return false
}
hostname := u.Hostname()
if hostname == "" {
return false
}
if net.ParseIP(hostname) == nil && !hostnameRegexRFC1123().MatchString(hostname) {
return false
}
portStr := u.Port()
if portStr != "" {
// Port 0 is reserved (RFC 6335) and has no valid use as an origin port.
// https://www.rfc-editor.org/rfc/rfc6335.html#section-6
port, portErr := strconv.ParseUint(portStr, 10, 16)
if portErr != nil || port == 0 {
return false
}
}
return true
}
panic(fmt.Sprintf("Bad field type %s", field.Type()))
}
// isURL is the validation function for validating if the current field's value is a valid URL.
func isURL(fl FieldLevel) bool {
field := fl.Field()
@@ -1635,6 +1729,63 @@ func isFile(fl FieldLevel) bool {
panic(fmt.Sprintf("Bad field type %s", field.Type()))
}
func detectFileMIMEType(field reflect.Value) (string, bool) {
switch field.Kind() {
case reflect.String:
filePath := field.String()
fileInfo, err := os.Stat(filePath)
if err != nil || fileInfo.IsDir() {
return "", false
}
file, err := os.Open(filePath)
if err != nil {
return "", false
}
defer func() {
_ = file.Close()
}()
mime, err := mimetype.DetectReader(file)
if err != nil {
return "", false
}
return mime.String(), true
}
panic(fmt.Sprintf("Bad field type %s", field.Type()))
}
func matchesMIMEType(mime, expected string) bool {
expectedType, expectedSubtype, ok := strings.Cut(strings.TrimSpace(expected), "/")
if !ok || expectedType == "" || expectedSubtype == "" {
return false
}
mimeType, mimeSubtype, ok := strings.Cut(mime, "/")
if !ok || mimeType == "" || mimeSubtype == "" {
return false
}
if expectedSubtype == "*" {
return mimeType == expectedType
}
return mimeType == expectedType && mimeSubtype == expectedSubtype
}
// isMIMEType is the validation function for validating if the current field's value contains the path to a file
// whose detected MIME type matches the provided validator param in the form type/subtype or type/*.
func isMIMEType(fl FieldLevel) bool {
mime, ok := detectFileMIMEType(fl.Field())
if !ok {
return false
}
return matchesMIMEType(mime, fl.Param())
}
// isImage is the validation function for validating if the current field's value contains the path to a valid image file
func isImage(fl FieldLevel) bool {
mimetypes := map[string]bool{
@@ -1663,39 +1814,14 @@ func isImage(fl FieldLevel) bool {
"image/x-xpixmap": true,
"image/x-xwindowdump": true,
}
field := fl.Field()
switch field.Kind() {
case reflect.String:
filePath := field.String()
fileInfo, err := os.Stat(filePath)
if err != nil {
return false
}
if fileInfo.IsDir() {
return false
}
file, err := os.Open(filePath)
if err != nil {
return false
}
defer func() {
_ = file.Close()
}()
mime, err := mimetype.DetectReader(file)
if err != nil {
return false
}
if _, ok := mimetypes[mime.String()]; ok {
return true
}
mime, ok := detectFileMIMEType(fl.Field())
if !ok {
return false
}
panic(fmt.Sprintf("Bad field type %s", field.Type()))
_, ok = mimetypes[mime]
return ok
}
// isFilePath is the validation function for validating if the current field's value is a valid file path.
@@ -3081,6 +3207,150 @@ func isBCP47LanguageTag(fl FieldLevel) bool {
panic(fmt.Sprintf("Bad field type %s", field.Type()))
}
// isBCP47StrictLanguageTag is the validation function for validating if the current field's value is a valid BCP 47 language tag
// according to https://www.rfc-editor.org/rfc/bcp/bcp47.txt
func isBCP47StrictLanguageTag(fl FieldLevel) bool {
field := fl.Field()
if field.Kind() != reflect.String {
panic(fmt.Sprintf("Bad field type %s", field.Type()))
}
lower := strings.ToLower(field.String())
lowerTagDash := lower + "-"
m := bcp47LanguageTagRe.FindStringSubmatch(lower)
if m == nil {
return false
}
grandfatheredOrPrivateuse := m[1]
lang := m[2]
script := m[3]
region := m[4]
variant := m[5]
extension := m[6]
if grandfatheredOrPrivateuse != "" {
return true
}
// language = 2*3ALPHA ; shortest ISO 639 code
// ["-" extlang] ; sometimes followed by
// ; extended language subtags
// / 4ALPHA ; or reserved for future use
// / 5*8ALPHA ; or registered language subtag
switch n := len(lang); {
// 2*3ALPHA "-" extlang
case strings.Contains(lang, "-"):
parts := strings.Split(lang, "-")
baseLang := parts[0]
base, err := language.ParseBase(baseLang)
if err != nil {
return false
}
// base.String() normalizes the base to the shortest code
// for the language
if base.String() != baseLang {
return false
}
for _, e := range parts[1:] {
prefixes, ok := iana_subtag_registry_extlangs[e]
if !ok {
return false
}
if len(prefixes) > 0 {
found := false
for _, p := range prefixes {
if strings.HasPrefix(lowerTagDash, p) {
found = true
break
}
}
if !found {
return false
}
}
}
// 2*3ALPHA ; shortest ISO 639 code
case n <= 3:
base, err := language.ParseBase(lang)
if err != nil {
return false
}
// base.String() normalizes the base to the shortest code
// for the language
if base.String() != lang {
return false
}
// 4ALPHA ; or reserved for future use
case n == 4:
return false
// 5*8ALPHA ; or registered language subtag
default:
// registered language subtag with 5+ characters.
// As of today there aren't any.
// https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry
return false
}
// script = 4ALPHA ; ISO 15924 code
if script != "" {
_, err := language.ParseScript(script)
if err != nil {
return false
}
}
// region = 2ALPHA ; ISO 3166-1 code
// 3DIGIT ; UN M.49 code
if region != "" {
if len(region) == 2 {
_, err := language.ParseRegion(region)
if err != nil {
return false
}
} else {
// Can't use language.ParseRegion() here because not all
// UN M.49 region codes are allowed, just the subset present
// in the IANA subtag registry.
_, ok := iana_subtag_registry_m49_codes[region]
if !ok {
return false
}
}
}
// variant = 5*8alphanum ; registered variants
// / (DIGIT 3alphanum)
if variant != "" {
for v := range strings.SplitSeq(variant, "-") {
_, err := language.ParseVariant(v)
if err != nil {
return false
}
_, ok := iana_subtag_registry_variants[v]
if !ok {
return false
}
}
}
if extension != "" {
_, err := language.ParseExtension(extension)
if err != nil {
return false
}
}
return true
}
// isIsoBic2014Format is the validation function for validating if the current field's value is a valid Business Identifier Code (SWIFT code), defined in ISO 9362 2014
func isIsoBic2014Format(fl FieldLevel) bool {
bicString := fl.Field().String()
@@ -3235,59 +3505,9 @@ func isEIN(fl FieldLevel) bool {
return einRegex().MatchString(field.String())
}
func isValidateFn(fl FieldLevel) bool {
const defaultParam = `Validate`
field := fl.Field()
validateFn := cmp.Or(fl.Param(), defaultParam)
ok, err := tryCallValidateFn(field, validateFn)
if err != nil {
return false
}
return ok
}
var (
errMethodNotFound = errors.New(`method not found`)
errMethodReturnNoValues = errors.New(`method return o values (void)`)
errMethodReturnInvalidType = errors.New(`method should return invalid type`)
)
func tryCallValidateFn(field reflect.Value, validateFn string) (bool, error) {
method := field.MethodByName(validateFn)
if field.CanAddr() && !method.IsValid() {
method = field.Addr().MethodByName(validateFn)
}
if !method.IsValid() {
return false, fmt.Errorf("unable to call %q on type %q: %w",
validateFn, field.Type().String(), errMethodNotFound)
}
returnValues := method.Call([]reflect.Value{})
if len(returnValues) == 0 {
return false, fmt.Errorf("unable to use result of method %q on type %q: %w",
validateFn, field.Type().String(), errMethodReturnNoValues)
}
firstReturnValue := returnValues[0]
switch firstReturnValue.Kind() {
case reflect.Bool:
return firstReturnValue.Bool(), nil
case reflect.Interface:
errorType := reflect.TypeOf((*error)(nil)).Elem()
if firstReturnValue.Type().Implements(errorType) {
return firstReturnValue.IsNil(), nil
}
return false, fmt.Errorf("unable to use result of method %q on type %q: %w (got interface %v expect error)",
validateFn, field.Type().String(), errMethodReturnInvalidType, firstReturnValue.Type().String())
default:
return false, fmt.Errorf("unable to use result of method %q on type %q: %w (got %v expect error or bool)",
validateFn, field.Type().String(), errMethodReturnInvalidType, firstReturnValue.Type().String())
}
}
+1 -1
View File
@@ -144,7 +144,7 @@ func (v *Validate) extractStructCache(current reflect.Value, sName string) *cStr
if v.hasTagNameFunc {
name := v.tagNameFunc(fld)
if len(name) > 0 {
if len(name) > 0 || v.omitBlankFieldNames {
customName = name
}
}
+12
View File
@@ -1175,3 +1175,15 @@ var iso3166_2 = map[string]struct{}{
"ZW-BU": {}, "ZW-HA": {}, "ZW-MA": {}, "ZW-MC": {}, "ZW-ME": {},
"ZW-MI": {}, "ZW-MN": {}, "ZW-MS": {}, "ZW-MV": {}, "ZW-MW": {},
}
// Subset of UN M.49 region codes present in the IANA Language Subtag Registry:
// https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry
var iana_subtag_registry_m49_codes = map[string]struct{}{
"001": {}, "002": {}, "003": {}, "005": {}, "009": {},
"011": {}, "013": {}, "014": {}, "015": {}, "017": {},
"018": {}, "019": {}, "021": {}, "029": {}, "030": {},
"034": {}, "035": {}, "039": {}, "053": {}, "054": {},
"057": {}, "061": {}, "142": {}, "143": {}, "145": {},
"150": {}, "151": {}, "154": {}, "155": {}, "202": {},
"419": {},
}
+42
View File
@@ -184,6 +184,12 @@ so the above will become excludesall=0x7C
Field `validate:"excludesall=0x7C"` // GOOD! Use the UTF-8 hex representation.
}
# Build tags
The library provides a build tag for build size optimizations. If you are not using
`validateFn` you can add the `validator_novalidatefn` build tag to enabled better dead
code elimination. With this build tag, any usage of `validateFn` tags will panic.
# Baked In Validators and Tags
Here is a list of the current built in validators:
@@ -546,6 +552,24 @@ Works the same as oneof but is case insensitive and therefore only accepts strin
Usage: oneofci=red green
oneofci='red green' 'blue yellow'
# None Of
For strings, ints, and uints, noneof will ensure that the value is not one of
the values in the parameter. The parameter should be a list of values separated by whitespace.
Values may be strings or numbers. To inversely match strings with spaces in them, include the target string between single quotes.
Kind of like an 'enum'.
Usage: noneof=red green
noneof='red green' 'blue yellow'
noneof=5 7 9
# None Of Case Insensitive
Works the same as noneof but is case insensitive and therefore only accepts strings.
Usage: noneofci=red green
noneofci='red green' 'blue yellow'
# Greater Than
For numbers, this will ensure that the value is greater than the
@@ -969,6 +993,16 @@ This is done using os.Stat and github.com/gabriel-vasile/mimetype
Usage: image
# MIME type path
This validates that a string value contains a valid file path and that
the file exists on the machine and matches the provided MIME type in the
form type/subtype or type/*.
This is done using os.Stat and github.com/gabriel-vasile/mimetype
Usage: mimetype=image/png
Usage: mimetype=image/*
# File Path
This validates that a string value contains a valid file path but does not
@@ -1442,6 +1476,14 @@ More information on https://pkg.go.dev/golang.org/x/text/language
Usage: bcp47_language_tag
# BCP 47 Strict Language Tag
This validates that a string value is a valid BCP 47 language tag strictly following RFC 5646 rules,
unlike language.Parse which also accepts Unicode extensions.
see https://www.rfc-editor.org/rfc/bcp/bcp47.txt
Usage: bcp47_strict_language_tag
BIC (SWIFT code - 2022 standard)
This validates that a string value is a valid Business Identifier Code (SWIFT code), defined in ISO 9362:2022.
+14
View File
@@ -24,3 +24,17 @@ func WithPrivateFieldValidation() Option {
v.privateFieldValidation = true
}
}
// WithTagNameFuncBlankOmit makes a blank return from a RegisterTagNameFunc omit
// the field from the error namespace instead of silently falling back to the
// struct field name.
//
// This was made opt-in behaviour to maintain backward compatibility with
// existing callers that rely on the fallback for error namespaces.
//
// It is recommended you enable this as it will be the default behaviour in v11+.
func WithTagNameFuncBlankOmit() Option {
return func(v *Validate) {
v.omitBlankFieldNames = true
}
}
+7 -7
View File
@@ -33,7 +33,7 @@ const (
uUID3RegexString = "^[0-9a-f]{8}-[0-9a-f]{4}-3[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$"
uUID4RegexString = "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
uUID5RegexString = "^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
uUIDRegexString = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
uUIDRegexString = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
uUID3RFC4122RegexString = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-3[0-9a-fA-F]{3}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
uUID4RFC4122RegexString = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$"
uUID5RFC4122RegexString = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-5[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$"
@@ -56,12 +56,12 @@ const (
latitudeRegexString = "^[-+]?([1-8]?\\d(\\.\\d+)?|90(\\.0+)?)$"
longitudeRegexString = "^[-+]?(180(\\.0+)?|((1[0-7]\\d)|([1-9]?\\d))(\\.\\d+)?)$"
sSNRegexString = `^[0-9]{3}[ -]?(0[1-9]|[1-9][0-9])[ -]?([1-9][0-9]{3}|[0-9][1-9][0-9]{2}|[0-9]{2}[1-9][0-9]|[0-9]{3}[1-9])$`
hostnameRegexStringRFC952 = `^[a-zA-Z]([a-zA-Z0-9\-]+[\.]?)*[a-zA-Z0-9]$` // https://tools.ietf.org/html/rfc952
hostnameRegexStringRFC1123 = `^([a-zA-Z0-9]{1}[a-zA-Z0-9-]{0,62}){1}(\.[a-zA-Z0-9]{1}[a-zA-Z0-9-]{0,62})*?$` // accepts hostname starting with a digit https://tools.ietf.org/html/rfc1123
hostnameRegexStringRFC952 = `^[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$` // https://tools.ietf.org/html/rfc952
hostnameRegexStringRFC1123 = `^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$` // accepts hostname starting with a digit https://tools.ietf.org/html/rfc1123
fqdnRegexStringRFC1123 = `^([a-zA-Z0-9]{1}[a-zA-Z0-9-]{0,62})(\.[a-zA-Z0-9]{1}[a-zA-Z0-9-]{0,62})*?(\.[a-zA-Z]{1}[a-zA-Z0-9-]{0,62})\.?$` // same as hostnameRegexStringRFC1123 but must contain a non numerical TLD (possibly ending with '.')
btcAddressRegexString = `^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$` // bitcoin address
btcAddressUpperRegexStringBech32 = `^BC1[02-9AC-HJ-NP-Z]{7,76}$` // bitcoin bech32 address https://en.bitcoin.it/wiki/Bech32
btcAddressLowerRegexStringBech32 = `^bc1[02-9ac-hj-np-z]{7,76}$` // bitcoin bech32 address https://en.bitcoin.it/wiki/Bech32
btcAddressRegexString = `^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$` // bitcoin address
btcAddressUpperRegexStringBech32 = `^BC1[02-9AC-HJ-NP-Z]{7,76}$` // bitcoin bech32 address https://en.bitcoin.it/wiki/Bech32
btcAddressLowerRegexStringBech32 = `^bc1[02-9ac-hj-np-z]{7,76}$` // bitcoin bech32 address https://en.bitcoin.it/wiki/Bech32
ethAddressRegexString = `^0x[0-9a-fA-F]{40}$`
ethAddressUpperRegexString = `^0x[0-9A-F]{40}$`
ethAddressLowerRegexString = `^0x[0-9a-f]{40}$`
@@ -77,7 +77,7 @@ const (
cveRegexString = `^CVE-(1999|2\d{3})-(0[^0]\d{2}|0\d[^0]\d{1}|0\d{2}[^0]|[1-9]{1}\d{3,})$` // CVE Format Id https://cve.mitre.org/cve/identifiers/syntaxchange.html
mongodbIdRegexString = "^[a-f\\d]{24}$"
mongodbConnStringRegexString = "^mongodb(\\+srv)?:\\/\\/(([a-zA-Z\\d]+):([a-zA-Z\\d$:\\/?#\\[\\]@]+)@)?(([a-z\\d.-]+)(:[\\d]+)?)((,(([a-z\\d.-]+)(:(\\d+))?))*)?(\\/[a-zA-Z-_]{1,64})?(\\?(([a-zA-Z]+)=([a-zA-Z\\d]+))(&(([a-zA-Z\\d]+)=([a-zA-Z\\d]+))?)*)?$"
cronRegexString = `(@(annually|yearly|monthly|weekly|daily|hourly|reboot))|(@every (\d+(ns|us|µs|ms|s|m|h))+)|((((\d+,)+\d+|((\*|\d+)(\/|-)\d+)|\d+|\*) ?){5,7})`
cronRegexString = `^((@(annually|yearly|monthly|weekly|daily|hourly|reboot))|(@every (\d+(ns|us|µs|ms|s|m|h))+)|(([A-Za-z0-9*?][A-Za-z0-9*?/,#L-]+|[*?0-9])( +([A-Za-z0-9*?][A-Za-z0-9*?/,#L-]+|[*?0-9])){4,6}))$`
spicedbIDRegexString = `^(([a-zA-Z0-9/_|\-=+]{1,})|\*)$`
spicedbPermissionRegexString = "^([a-z][a-z0-9_]{1,62}[a-z0-9])?$"
spicedbTypeRegexString = "^([a-z][a-z0-9_]{1,61}[a-z0-9]/)?[a-z][a-z0-9_]{1,62}[a-z0-9]$"
+15
View File
@@ -1466,6 +1466,11 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er
return t
},
},
{
tag: "timezone",
translation: "{0} must be a valid time zone",
override: false,
},
{
tag: "postcode_iso3166_alpha2",
translation: "{0} does not match postcode format of {1} country",
@@ -1504,11 +1509,21 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er
translation: "{0} must be a valid image",
override: false,
},
{
tag: "mimetype",
translation: "{0} must be a valid MIME type",
override: false,
},
{
tag: "cve",
translation: "{0} must be a valid cve identifier",
override: false,
},
{
tag: "bcp47_strict_language_tag",
translation: "{0} must be a valid BCP 47 language tag",
override: false,
},
{
tag: "validateFn",
translation: "{0} must be a valid object",
+20 -6
View File
@@ -116,7 +116,7 @@ func (v *validate) traverseField(ctx context.Context, parent reflect.Value, curr
if ct.hasTag {
if kind == reflect.Invalid {
v.str1 = string(append(ns, cf.altName...))
v.str1 = appendAltName(ns, cf.altName)
if v.v.hasTagNameFunc {
v.str2 = string(append(structNs, cf.name...))
} else {
@@ -138,7 +138,7 @@ func (v *validate) traverseField(ctx context.Context, parent reflect.Value, curr
return
}
v.str1 = string(append(ns, cf.altName...))
v.str1 = appendAltName(ns, cf.altName)
if v.v.hasTagNameFunc {
v.str2 = string(append(structNs, cf.name...))
} else {
@@ -192,7 +192,9 @@ OUTER:
// VarWithField - this allows for validating against each field within the struct against a specific value
// pretty handy in certain situations
if len(cf.name) > 0 {
ns = append(append(ns, cf.altName...), '.')
if len(cf.altName) > 0 {
ns = append(append(ns, cf.altName...), '.')
}
structNs = append(append(structNs, cf.name...), '.')
}
@@ -212,7 +214,9 @@ OUTER:
// VarWithField - this allows for validating against each field within the struct against a specific value
// pretty handy in certain situations
if len(cf.name) > 0 {
ns = append(append(ns, cf.altName...), '.')
if len(cf.altName) > 0 {
ns = append(append(ns, cf.altName...), '.')
}
structNs = append(append(structNs, cf.name...), '.')
}
@@ -409,7 +413,7 @@ OUTER:
if ct.isBlockEnd || ct.next == nil {
// if we get here, no valid 'or' value and no more tags
v.str1 = string(append(ns, cf.altName...))
v.str1 = appendAltName(ns, cf.altName)
if v.v.hasTagNameFunc {
v.str2 = string(append(structNs, cf.name...))
@@ -468,7 +472,7 @@ OUTER:
v.ct = ct
if !ct.fn(ctx, v) {
v.str1 = string(append(ns, cf.altName...))
v.str1 = appendAltName(ns, cf.altName)
if v.v.hasTagNameFunc {
v.str2 = string(append(structNs, cf.name...))
@@ -499,6 +503,16 @@ OUTER:
}
}
func appendAltName(ns []byte, altName string) string {
if len(altName) > 0 {
return string(append(ns, altName...))
}
if n := len(ns); n > 0 && ns[n-1] == '.' {
return string(ns[:n-1])
}
return string(ns)
}
func getValue(val reflect.Value) interface{} {
if val.CanInterface() {
return val.Interface()
+6 -5
View File
@@ -68,7 +68,7 @@ type FilterFunc func(ns []byte) bool
// CustomTypeFunc allows for overriding or adding custom field type handler functions
// field = field value of the type to return a value to be validated
// example Valuer from sql drive see https://golang.org/src/database/sql/driver/types.go?s=1210:1293#L29
// example Valuer from sql driver see https://golang.org/src/database/sql/driver/types.go?s=1210:1293#L29
type CustomTypeFunc func(field reflect.Value) interface{}
// TagNameFunc allows for adding of a custom tag name parser
@@ -96,6 +96,7 @@ type Validate struct {
hasTagNameFunc bool
requiredStructEnabled bool
privateFieldValidation bool
omitBlankFieldNames bool
}
// New returns a new instance of 'validate' with sane defaults.
@@ -428,7 +429,7 @@ func (v *Validate) StructFilteredCtx(ctx context.Context, s interface{}, fn Filt
}
// StructPartial validates the fields passed in only, ignoring all others.
// Fields may be provided in a namespaced fashion relative to the struct provided
// Fields may be provided in a namespaced fashion relative to the struct provided
// eg. NestedStruct.Field or NestedArrayField[0].Struct.Name
//
// It returns InvalidValidationError for bad values passed in and nil or ValidationErrors as error otherwise.
@@ -439,7 +440,7 @@ func (v *Validate) StructPartial(s interface{}, fields ...string) error {
// StructPartialCtx validates the fields passed in only, ignoring all others and allows passing of contextual
// validation information via context.Context
// Fields may be provided in a namespaced fashion relative to the struct provided
// Fields may be provided in a namespaced fashion relative to the struct provided
// eg. NestedStruct.Field or NestedArrayField[0].Struct.Name
//
// It returns InvalidValidationError for bad values passed in and nil or ValidationErrors as error otherwise.
@@ -514,7 +515,7 @@ func (v *Validate) StructPartialCtx(ctx context.Context, s interface{}, fields .
}
// StructExcept validates all fields except the ones passed in.
// Fields may be provided in a namespaced fashion relative to the struct provided
// Fields may be provided in a namespaced fashion relative to the struct provided
// i.e. NestedStruct.Field or NestedArrayField[0].Struct.Name
//
// It returns InvalidValidationError for bad values passed in and nil or ValidationErrors as error otherwise.
@@ -525,7 +526,7 @@ func (v *Validate) StructExcept(s interface{}, fields ...string) error {
// StructExceptCtx validates all fields except the ones passed in and allows passing of contextual
// validation information via context.Context
// Fields may be provided in a namespaced fashion relative to the struct provided
// Fields may be provided in a namespaced fashion relative to the struct provided
// i.e. NestedStruct.Field or NestedArrayField[0].Struct.Name
//
// It returns InvalidValidationError for bad values passed in and nil or ValidationErrors as error otherwise.
+3
View File
@@ -19,3 +19,6 @@ indent_size = 2
[.golangci.yaml]
indent_size = 2
[devenv.yaml]
indent_size = 2
+7 -3
View File
@@ -1,6 +1,10 @@
/.devenv/
/.direnv/
/.pre-commit-config.yaml
/bin/
/build/
/var/
# Devenv
.devenv*
devenv.local.nix
devenv.local.yaml
.direnv
.pre-commit-config.yaml
-294
View File
@@ -1,294 +0,0 @@
{
"nodes": {
"cachix": {
"inputs": {
"devenv": [
"devenv"
],
"flake-compat": [
"devenv"
],
"git-hooks": [
"devenv"
],
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1742042642,
"narHash": "sha256-D0gP8srrX0qj+wNYNPdtVJsQuFzIng3q43thnHXQ/es=",
"owner": "cachix",
"repo": "cachix",
"rev": "a624d3eaf4b1d225f918de8543ed739f2f574203",
"type": "github"
},
"original": {
"owner": "cachix",
"ref": "latest",
"repo": "cachix",
"type": "github"
}
},
"devenv": {
"inputs": {
"cachix": "cachix",
"flake-compat": "flake-compat",
"git-hooks": "git-hooks",
"nix": "nix",
"nixpkgs": "nixpkgs_3"
},
"locked": {
"lastModified": 1744876578,
"narHash": "sha256-8MTBj2REB8t29sIBLpxbR0+AEGJ7f+RkzZPAGsFd40c=",
"owner": "cachix",
"repo": "devenv",
"rev": "7ff7c351bba20d0615be25ecdcbcf79b57b85fe1",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "devenv",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1733328505,
"narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": [
"devenv",
"nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1712014858,
"narHash": "sha256-sB4SWl2lX95bExY2gMFG5HIzvva5AVMJd4Igm+GpZNw=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "9126214d0a59633752a136528f5f3b9aa8565b7d",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-parts_2": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1743550720,
"narHash": "sha256-hIshGgKZCgWh6AYJpJmRgFdR3WUbkY04o82X05xqQiY=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "c621e8422220273271f52058f618c94e405bb0f5",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"git-hooks": {
"inputs": {
"flake-compat": [
"devenv"
],
"gitignore": "gitignore",
"nixpkgs": [
"devenv",
"nixpkgs"
]
},
"locked": {
"lastModified": 1742649964,
"narHash": "sha256-DwOTp7nvfi8mRfuL1escHDXabVXFGT1VlPD1JHrtrco=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "dcf5072734cb576d2b0c59b2ac44f5050b5eac82",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "git-hooks.nix",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"devenv",
"git-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"libgit2": {
"flake": false,
"locked": {
"lastModified": 1697646580,
"narHash": "sha256-oX4Z3S9WtJlwvj0uH9HlYcWv+x1hqp8mhXl7HsLu2f0=",
"owner": "libgit2",
"repo": "libgit2",
"rev": "45fd9ed7ae1a9b74b957ef4f337bc3c8b3df01b5",
"type": "github"
},
"original": {
"owner": "libgit2",
"repo": "libgit2",
"type": "github"
}
},
"nix": {
"inputs": {
"flake-compat": [
"devenv"
],
"flake-parts": "flake-parts",
"libgit2": "libgit2",
"nixpkgs": "nixpkgs_2",
"nixpkgs-23-11": [
"devenv"
],
"nixpkgs-regression": [
"devenv"
],
"pre-commit-hooks": [
"devenv"
]
},
"locked": {
"lastModified": 1741798497,
"narHash": "sha256-E3j+3MoY8Y96mG1dUIiLFm2tZmNbRvSiyN7CrSKuAVg=",
"owner": "domenkozar",
"repo": "nix",
"rev": "f3f44b2baaf6c4c6e179de8cbb1cc6db031083cd",
"type": "github"
},
"original": {
"owner": "domenkozar",
"ref": "devenv-2.24",
"repo": "nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1733212471,
"narHash": "sha256-M1+uCoV5igihRfcUKrr1riygbe73/dzNnzPsmaLCmpo=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "55d15ad12a74eb7d4646254e13638ad0c4128776",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-lib": {
"locked": {
"lastModified": 1743296961,
"narHash": "sha256-b1EdN3cULCqtorQ4QeWgLMrd5ZGOjLSLemfa00heasc=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "e4822aea2a6d1cdd36653c134cacfd64c97ff4fa",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixpkgs.lib",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1717432640,
"narHash": "sha256-+f9c4/ZX5MWDOuB1rKoWj+lBNm0z0rs4CK47HBLxy1o=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "88269ab3044128b7c2f4c7d68448b2fb50456870",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "release-24.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1733477122,
"narHash": "sha256-qamMCz5mNpQmgBwc8SB5tVMlD5sbwVIToVZtSxMph9s=",
"owner": "cachix",
"repo": "devenv-nixpkgs",
"rev": "7bd9e84d0452f6d2e63b6e6da29fe73fac951857",
"type": "github"
},
"original": {
"owner": "cachix",
"ref": "rolling",
"repo": "devenv-nixpkgs",
"type": "github"
}
},
"nixpkgs_4": {
"locked": {
"lastModified": 1744536153,
"narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"devenv": "devenv",
"flake-parts": "flake-parts_2",
"nixpkgs": "nixpkgs_4"
}
}
},
"root": "root",
"version": 7
}
-46
View File
@@ -1,46 +0,0 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
flake-parts.url = "github:hercules-ci/flake-parts";
devenv.url = "github:cachix/devenv";
};
outputs =
inputs@{ flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
imports = [
inputs.devenv.flakeModule
];
systems = [
"x86_64-linux"
"x86_64-darwin"
"aarch64-darwin"
];
perSystem =
{ pkgs, ... }:
rec {
devenv.shells = {
default = {
languages = {
go.enable = true;
};
pre-commit.hooks = {
nixpkgs-fmt.enable = true;
};
packages = with pkgs; [
golangci-lint
];
# https://github.com/cachix/devenv/issues/528#issuecomment-1556108767
containers = pkgs.lib.mkForce { };
};
ci = devenv.shells.default;
};
};
};
}
+284 -48
View File
@@ -173,6 +173,25 @@
// Public: "I made it through!"
// }
//
// # Custom Decoding with Unmarshaler
//
// Types can implement the Unmarshaler interface to control their own decoding. The interface
// behaves similarly to how UnmarshalJSON does in the standard library. It can be used as an
// alternative or companion to a DecodeHook.
//
// type TrimmedString string
//
// func (t *TrimmedString) UnmarshalMapstructure(input any) error {
// str, ok := input.(string)
// if !ok {
// return fmt.Errorf("expected string, got %T", input)
// }
// *t = TrimmedString(strings.TrimSpace(str))
// return nil
// }
//
// See the Unmarshaler interface documentation for more details.
//
// # Other Configuration
//
// mapstructure is highly configurable. See the DecoderConfig struct
@@ -218,6 +237,17 @@ type DecodeHookFuncKind func(reflect.Kind, reflect.Kind, any) (any, error)
// values.
type DecodeHookFuncValue func(from reflect.Value, to reflect.Value) (any, error)
// Unmarshaler is the interface implemented by types that can unmarshal
// themselves. UnmarshalMapstructure receives the input data (potentially
// transformed by DecodeHook) and should populate the receiver with the
// decoded values.
//
// The Unmarshaler interface takes precedence over the default decoding
// logic for any type (structs, slices, maps, primitives, etc.).
type Unmarshaler interface {
UnmarshalMapstructure(any) error
}
// DecoderConfig is the configuration that is used to create a new decoder
// and allows customization of various aspects of decoding.
type DecoderConfig struct {
@@ -281,6 +311,13 @@ type DecoderConfig struct {
// }
Squash bool
// Deep will map structures in slices instead of copying them
//
// type Parent struct {
// Children []Child `mapstructure:",deep"`
// }
Deep bool
// Metadata is the struct that will contain extra metadata about
// the decoding. If this is nil, then no metadata will be tracked.
Metadata *Metadata
@@ -290,9 +327,15 @@ type DecoderConfig struct {
Result any
// The tag name that mapstructure reads for field names. This
// defaults to "mapstructure"
// defaults to "mapstructure". Multiple tag names can be specified
// as a comma-separated list (e.g., "yaml,json"), and the first
// matching non-empty tag will be used.
TagName string
// RootName specifies the name to use for the root element in error messages. For example:
// '<rootName>' has unset fields: <fieldName>
RootName string
// The option of the value in the tag that indicates a field should
// be squashed. This defaults to "squash".
SquashTagOption string
@@ -304,11 +347,34 @@ type DecoderConfig struct {
// MatchName is the function used to match the map key to the struct
// field name or tag. Defaults to `strings.EqualFold`. This can be used
// to implement case-sensitive tag values, support snake casing, etc.
//
// MatchName is used as a fallback comparison when the direct key lookup fails.
// See also MapFieldName for transforming field names before lookup.
MatchName func(mapKey, fieldName string) bool
// DecodeNil, if set to true, will cause the DecodeHook (if present) to run
// even if the input is nil. This can be used to provide default values.
DecodeNil bool
// MapFieldName is the function used to convert the struct field name to the map's key name.
//
// This is useful for automatically converting between naming conventions without
// explicitly tagging each field. For example, to convert Go's PascalCase field names
// to snake_case map keys:
//
// MapFieldName: func(s string) string {
// return strcase.ToSnake(s)
// }
//
// When decoding from a map to a struct, the transformed field name is used for
// the initial lookup. If not found, MatchName is used as a fallback comparison.
// Explicit struct tags always take precedence over MapFieldName.
MapFieldName func(string) string
// DisableUnmarshaler, if set to true, disables the use of the Unmarshaler
// interface. Types implementing Unmarshaler will be decoded using the
// standard struct decoding logic instead.
DisableUnmarshaler bool
}
// A Decoder takes a raw interface value and turns it into structured
@@ -445,6 +511,12 @@ func NewDecoder(config *DecoderConfig) (*Decoder, error) {
config.MatchName = strings.EqualFold
}
if config.MapFieldName == nil {
config.MapFieldName = func(s string) string {
return s
}
}
result := &Decoder{
config: config,
}
@@ -458,7 +530,7 @@ func NewDecoder(config *DecoderConfig) (*Decoder, error) {
// Decode decodes the given raw interface to the target pointer specified
// by the configuration.
func (d *Decoder) Decode(input any) error {
err := d.decode("", input, reflect.ValueOf(d.config.Result).Elem())
err := d.decode(d.config.RootName, input, reflect.ValueOf(d.config.Result).Elem())
// Retain some of the original behavior when multiple errors ocurr
var joinedErr interface{ Unwrap() []error }
@@ -540,36 +612,50 @@ func (d *Decoder) decode(name string, input any, outVal reflect.Value) error {
var err error
addMetaKey := true
switch outputKind {
case reflect.Bool:
err = d.decodeBool(name, input, outVal)
case reflect.Interface:
err = d.decodeBasic(name, input, outVal)
case reflect.String:
err = d.decodeString(name, input, outVal)
case reflect.Int:
err = d.decodeInt(name, input, outVal)
case reflect.Uint:
err = d.decodeUint(name, input, outVal)
case reflect.Float32:
err = d.decodeFloat(name, input, outVal)
case reflect.Complex64:
err = d.decodeComplex(name, input, outVal)
case reflect.Struct:
err = d.decodeStruct(name, input, outVal)
case reflect.Map:
err = d.decodeMap(name, input, outVal)
case reflect.Ptr:
addMetaKey, err = d.decodePtr(name, input, outVal)
case reflect.Slice:
err = d.decodeSlice(name, input, outVal)
case reflect.Array:
err = d.decodeArray(name, input, outVal)
case reflect.Func:
err = d.decodeFunc(name, input, outVal)
default:
// If we reached this point then we weren't able to decode it
return newDecodeError(name, fmt.Errorf("unsupported type: %s", outputKind))
// Check if the target implements Unmarshaler and use it if not disabled
unmarshaled := false
if !d.config.DisableUnmarshaler {
if unmarshaler, ok := getUnmarshaler(outVal); ok {
if err = unmarshaler.UnmarshalMapstructure(input); err != nil {
err = newDecodeError(name, err)
}
unmarshaled = true
}
}
if !unmarshaled {
switch outputKind {
case reflect.Bool:
err = d.decodeBool(name, input, outVal)
case reflect.Interface:
err = d.decodeBasic(name, input, outVal)
case reflect.String:
err = d.decodeString(name, input, outVal)
case reflect.Int:
err = d.decodeInt(name, input, outVal)
case reflect.Uint:
err = d.decodeUint(name, input, outVal)
case reflect.Float32:
err = d.decodeFloat(name, input, outVal)
case reflect.Complex64:
err = d.decodeComplex(name, input, outVal)
case reflect.Struct:
err = d.decodeStruct(name, input, outVal)
case reflect.Map:
err = d.decodeMap(name, input, outVal)
case reflect.Ptr:
addMetaKey, err = d.decodePtr(name, input, outVal)
case reflect.Slice:
err = d.decodeSlice(name, input, outVal)
case reflect.Array:
err = d.decodeArray(name, input, outVal)
case reflect.Func:
err = d.decodeFunc(name, input, outVal)
default:
// If we reached this point then we weren't able to decode it
return newDecodeError(name, fmt.Errorf("unsupported type: %s", outputKind))
}
}
// If we reached here, then we successfully decoded SOMETHING, so
@@ -668,7 +754,7 @@ func (d *Decoder) decodeString(name string, data any, val reflect.Value) error {
case reflect.Uint8:
var uints []uint8
if dataKind == reflect.Array {
uints = make([]uint8, dataVal.Len(), dataVal.Len())
uints = make([]uint8, dataVal.Len())
for i := range uints {
uints[i] = dataVal.Index(i).Interface().(uint8)
}
@@ -1060,8 +1146,8 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re
)
}
tagValue := f.Tag.Get(d.config.TagName)
keyName := f.Name
tagValue, _ := getTagValue(f, d.config.TagName)
keyName := d.config.MapFieldName(f.Name)
if tagValue == "" && d.config.IgnoreUntaggedFields {
continue
@@ -1070,6 +1156,9 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re
// If Squash is set in the config, we squash the field down.
squash := d.config.Squash && v.Kind() == reflect.Struct && f.Anonymous
// If Deep is set in the config, set as default value.
deep := d.config.Deep
v = dereferencePtrToStructIfNeeded(v, d.config.TagName)
// Determine the name of the key in the map
@@ -1078,12 +1167,12 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re
continue
}
// If "omitempty" is specified in the tag, it ignores empty values.
if strings.Index(tagValue[index+1:], "omitempty") != -1 && isEmptyValue(v) {
if strings.Contains(tagValue[index+1:], "omitempty") && isEmptyValue(v) {
continue
}
// If "omitzero" is specified in the tag, it ignores zero values.
if strings.Index(tagValue[index+1:], "omitzero") != -1 && v.IsZero() {
if strings.Contains(tagValue[index+1:], "omitzero") && v.IsZero() {
continue
}
@@ -1103,7 +1192,7 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re
)
}
} else {
if strings.Index(tagValue[index+1:], "remain") != -1 {
if strings.Contains(tagValue[index+1:], "remain") {
if v.Kind() != reflect.Map {
return newDecodeError(
name+"."+f.Name,
@@ -1118,6 +1207,9 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re
continue
}
}
deep = deep || strings.Contains(tagValue[index+1:], "deep")
if keyNameTagValue := tagValue[:index]; keyNameTagValue != "" {
keyName = keyNameTagValue
}
@@ -1164,6 +1256,41 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re
valMap.SetMapIndex(reflect.ValueOf(keyName), vMap)
}
case reflect.Slice:
if deep {
var childType reflect.Type
switch v.Type().Elem().Kind() {
case reflect.Struct:
childType = reflect.TypeOf(map[string]any{})
default:
childType = v.Type().Elem()
}
sType := reflect.SliceOf(childType)
addrVal := reflect.New(sType)
vSlice := reflect.MakeSlice(sType, v.Len(), v.Cap())
if v.Len() > 0 {
reflect.Indirect(addrVal).Set(vSlice)
err := d.decode(keyName, v.Interface(), reflect.Indirect(addrVal))
if err != nil {
return err
}
}
vSlice = reflect.Indirect(addrVal)
valMap.SetMapIndex(reflect.ValueOf(keyName), vSlice)
break
}
// When deep mapping is not needed, fallthrough to normal copy
fallthrough
default:
valMap.SetMapIndex(reflect.ValueOf(keyName), v)
}
@@ -1471,7 +1598,10 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
remain := false
// We always parse the tags cause we're looking for other tags too
tagParts := strings.Split(fieldType.Tag.Get(d.config.TagName), ",")
tagParts := getTagParts(fieldType, d.config.TagName)
if len(tagParts) == 0 {
tagParts = []string{""}
}
for _, tag := range tagParts[1:] {
if tag == d.config.SquashTagOption {
squash = true
@@ -1492,6 +1622,18 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
if !fieldVal.IsNil() {
structs = append(structs, fieldVal.Elem().Elem())
}
case reflect.Ptr:
if fieldVal.Type().Elem().Kind() == reflect.Struct {
if fieldVal.IsNil() {
fieldVal.Set(reflect.New(fieldVal.Type().Elem()))
}
structs = append(structs, fieldVal.Elem())
} else {
errs = append(errs, newDecodeError(
name+"."+fieldType.Name,
fmt.Errorf("unsupported type for squashed pointer: %s", fieldVal.Type().Elem().Kind()),
))
}
default:
errs = append(errs, newDecodeError(
name+"."+fieldType.Name,
@@ -1516,13 +1658,15 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
field, fieldValue := f.field, f.val
fieldName := field.Name
tagValue := field.Tag.Get(d.config.TagName)
tagValue, _ := getTagValue(field, d.config.TagName)
if tagValue == "" && d.config.IgnoreUntaggedFields {
continue
}
tagValue = strings.SplitN(tagValue, ",", 2)[0]
if tagValue != "" {
fieldName = tagValue
} else {
fieldName = d.config.MapFieldName(fieldName)
}
rawMapKey := reflect.ValueOf(fieldName)
@@ -1605,8 +1749,14 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
}
sort.Strings(keys)
// Improve error message when name is empty by showing the target struct type
// in the case where it is empty for embedded structs.
errorName := name
if errorName == "" {
errorName = val.Type().String()
}
errs = append(errs, newDecodeError(
name,
errorName,
fmt.Errorf("has invalid keys: %s", strings.Join(keys, ", ")),
))
}
@@ -1692,7 +1842,7 @@ func isStructTypeConvertibleToMap(typ reflect.Type, checkMapstructureTags bool,
if f.PkgPath == "" && !checkMapstructureTags { // check for unexported fields
return true
}
if checkMapstructureTags && f.Tag.Get(tagName) != "" { // check for mapstructure tags inside
if checkMapstructureTags && hasAnyTag(f, tagName) { // check for mapstructure tags inside
return true
}
}
@@ -1700,13 +1850,99 @@ func isStructTypeConvertibleToMap(typ reflect.Type, checkMapstructureTags bool,
}
func dereferencePtrToStructIfNeeded(v reflect.Value, tagName string) reflect.Value {
if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct {
if v.Kind() != reflect.Ptr {
return v
}
deref := v.Elem()
derefT := deref.Type()
if isStructTypeConvertibleToMap(derefT, true, tagName) {
return deref
switch v.Elem().Kind() {
case reflect.Slice:
return v.Elem()
case reflect.Struct:
deref := v.Elem()
derefT := deref.Type()
if isStructTypeConvertibleToMap(derefT, true, tagName) {
return deref
}
return v
default:
return v
}
return v
}
func hasAnyTag(field reflect.StructField, tagName string) bool {
_, ok := getTagValue(field, tagName)
return ok
}
func getTagParts(field reflect.StructField, tagName string) []string {
tagValue, ok := getTagValue(field, tagName)
if !ok {
return nil
}
return strings.Split(tagValue, ",")
}
func getTagValue(field reflect.StructField, tagName string) (string, bool) {
for _, name := range splitTagNames(tagName) {
if tag := field.Tag.Get(name); tag != "" {
return tag, true
}
}
return "", false
}
func splitTagNames(tagName string) []string {
if tagName == "" {
return []string{"mapstructure"}
}
parts := strings.Split(tagName, ",")
result := make([]string, 0, len(parts))
for _, name := range parts {
name = strings.TrimSpace(name)
if name != "" {
result = append(result, name)
}
}
return result
}
// unmarshalerType is cached for performance
var unmarshalerType = reflect.TypeOf((*Unmarshaler)(nil)).Elem()
// getUnmarshaler checks if the value implements Unmarshaler and returns
// the Unmarshaler and a boolean indicating if it was found. It handles both
// pointer and value receivers.
func getUnmarshaler(val reflect.Value) (Unmarshaler, bool) {
// Skip invalid or nil values
if !val.IsValid() {
return nil, false
}
switch val.Kind() {
case reflect.Pointer, reflect.Interface:
if val.IsNil() {
return nil, false
}
}
// Check pointer receiver first (most common case)
if val.CanAddr() {
ptrVal := val.Addr()
// Quick check: if no methods, can't implement any interface
if ptrVal.Type().NumMethod() > 0 && ptrVal.Type().Implements(unmarshalerType) {
return ptrVal.Interface().(Unmarshaler), true
}
}
// Check value receiver
// Quick check: if no methods, can't implement any interface
if val.Type().NumMethod() > 0 && val.CanInterface() && val.Type().Implements(unmarshalerType) {
return val.Interface().(Unmarshaler), true
}
return nil, false
}
-96
View File
@@ -1,96 +0,0 @@
## 1.5.0
* New option `IgnoreUntaggedFields` to ignore decoding to any fields
without `mapstructure` (or the configured tag name) set [GH-277]
* New option `ErrorUnset` which makes it an error if any fields
in a target struct are not set by the decoding process. [GH-225]
* New function `OrComposeDecodeHookFunc` to help compose decode hooks. [GH-240]
* Decoding to slice from array no longer crashes [GH-265]
* Decode nested struct pointers to map [GH-271]
* Fix issue where `,squash` was ignored if `Squash` option was set. [GH-280]
* Fix issue where fields with `,omitempty` would sometimes decode
into a map with an empty string key [GH-281]
## 1.4.3
* Fix cases where `json.Number` didn't decode properly [GH-261]
## 1.4.2
* Custom name matchers to support any sort of casing, formatting, etc. for
field names. [GH-250]
* Fix possible panic in ComposeDecodeHookFunc [GH-251]
## 1.4.1
* Fix regression where `*time.Time` value would be set to empty and not be sent
to decode hooks properly [GH-232]
## 1.4.0
* A new decode hook type `DecodeHookFuncValue` has been added that has
access to the full values. [GH-183]
* Squash is now supported with embedded fields that are struct pointers [GH-205]
* Empty strings will convert to 0 for all numeric types when weakly decoding [GH-206]
## 1.3.3
* Decoding maps from maps creates a settable value for decode hooks [GH-203]
## 1.3.2
* Decode into interface type with a struct value is supported [GH-187]
## 1.3.1
* Squash should only squash embedded structs. [GH-194]
## 1.3.0
* Added `",omitempty"` support. This will ignore zero values in the source
structure when encoding. [GH-145]
## 1.2.3
* Fix duplicate entries in Keys list with pointer values. [GH-185]
## 1.2.2
* Do not add unsettable (unexported) values to the unused metadata key
or "remain" value. [GH-150]
## 1.2.1
* Go modules checksum mismatch fix
## 1.2.0
* Added support to capture unused values in a field using the `",remain"` value
in the mapstructure tag. There is an example to showcase usage.
* Added `DecoderConfig` option to always squash embedded structs
* `json.Number` can decode into `uint` types
* Empty slices are preserved and not replaced with nil slices
* Fix panic that can occur in when decoding a map into a nil slice of structs
* Improved package documentation for godoc
## 1.1.2
* Fix error when decode hook decodes interface implementation into interface
type. [GH-140]
## 1.1.1
* Fix panic that can happen in `decodePtr`
## 1.1.0
* Added `StringToIPHookFunc` to convert `string` to `net.IP` and `net.IPNet` [GH-133]
* Support struct to struct decoding [GH-137]
* If source map value is nil, then destination map value is nil (instead of empty)
* If source slice value is nil, then destination slice value is nil (instead of empty)
* If source pointer is nil, then destination pointer is set to nil (instead of
allocated zero value of type)
## 1.0.0
* Initial tagged stable release.
-21
View File
@@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2013 Mitchell Hashimoto
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-46
View File
@@ -1,46 +0,0 @@
# mapstructure [![Godoc](https://godoc.org/github.com/mitchellh/mapstructure?status.svg)](https://godoc.org/github.com/mitchellh/mapstructure)
mapstructure is a Go library for decoding generic map values to structures
and vice versa, while providing helpful error handling.
This library is most useful when decoding values from some data stream (JSON,
Gob, etc.) where you don't _quite_ know the structure of the underlying data
until you read a part of it. You can therefore read a `map[string]interface{}`
and use this library to decode it into the proper underlying native Go
structure.
## Installation
Standard `go get`:
```
$ go get github.com/mitchellh/mapstructure
```
## Usage & Example
For usage and examples see the [Godoc](http://godoc.org/github.com/mitchellh/mapstructure).
The `Decode` function has examples associated with it there.
## But Why?!
Go offers fantastic standard libraries for decoding formats such as JSON.
The standard method is to have a struct pre-created, and populate that struct
from the bytes of the encoded format. This is great, but the problem is if
you have configuration or an encoding that changes slightly depending on
specific fields. For example, consider this JSON:
```json
{
"type": "person",
"name": "Mitchell"
}
```
Perhaps we can't populate a specific structure without first reading
the "type" field from the JSON. We could always do two passes over the
decoding of the JSON (reading the "type" first, and the rest later).
However, it is much simpler to just decode this into a `map[string]interface{}`
structure, read the "type" key, then use something like this library
to decode it into the proper structure.
-279
View File
@@ -1,279 +0,0 @@
package mapstructure
import (
"encoding"
"errors"
"fmt"
"net"
"reflect"
"strconv"
"strings"
"time"
)
// typedDecodeHook takes a raw DecodeHookFunc (an interface{}) and turns
// it into the proper DecodeHookFunc type, such as DecodeHookFuncType.
func typedDecodeHook(h DecodeHookFunc) DecodeHookFunc {
// Create variables here so we can reference them with the reflect pkg
var f1 DecodeHookFuncType
var f2 DecodeHookFuncKind
var f3 DecodeHookFuncValue
// Fill in the variables into this interface and the rest is done
// automatically using the reflect package.
potential := []interface{}{f1, f2, f3}
v := reflect.ValueOf(h)
vt := v.Type()
for _, raw := range potential {
pt := reflect.ValueOf(raw).Type()
if vt.ConvertibleTo(pt) {
return v.Convert(pt).Interface()
}
}
return nil
}
// DecodeHookExec executes the given decode hook. This should be used
// since it'll naturally degrade to the older backwards compatible DecodeHookFunc
// that took reflect.Kind instead of reflect.Type.
func DecodeHookExec(
raw DecodeHookFunc,
from reflect.Value, to reflect.Value) (interface{}, error) {
switch f := typedDecodeHook(raw).(type) {
case DecodeHookFuncType:
return f(from.Type(), to.Type(), from.Interface())
case DecodeHookFuncKind:
return f(from.Kind(), to.Kind(), from.Interface())
case DecodeHookFuncValue:
return f(from, to)
default:
return nil, errors.New("invalid decode hook signature")
}
}
// ComposeDecodeHookFunc creates a single DecodeHookFunc that
// automatically composes multiple DecodeHookFuncs.
//
// The composed funcs are called in order, with the result of the
// previous transformation.
func ComposeDecodeHookFunc(fs ...DecodeHookFunc) DecodeHookFunc {
return func(f reflect.Value, t reflect.Value) (interface{}, error) {
var err error
data := f.Interface()
newFrom := f
for _, f1 := range fs {
data, err = DecodeHookExec(f1, newFrom, t)
if err != nil {
return nil, err
}
newFrom = reflect.ValueOf(data)
}
return data, nil
}
}
// OrComposeDecodeHookFunc executes all input hook functions until one of them returns no error. In that case its value is returned.
// If all hooks return an error, OrComposeDecodeHookFunc returns an error concatenating all error messages.
func OrComposeDecodeHookFunc(ff ...DecodeHookFunc) DecodeHookFunc {
return func(a, b reflect.Value) (interface{}, error) {
var allErrs string
var out interface{}
var err error
for _, f := range ff {
out, err = DecodeHookExec(f, a, b)
if err != nil {
allErrs += err.Error() + "\n"
continue
}
return out, nil
}
return nil, errors.New(allErrs)
}
}
// StringToSliceHookFunc returns a DecodeHookFunc that converts
// string to []string by splitting on the given sep.
func StringToSliceHookFunc(sep string) DecodeHookFunc {
return func(
f reflect.Kind,
t reflect.Kind,
data interface{}) (interface{}, error) {
if f != reflect.String || t != reflect.Slice {
return data, nil
}
raw := data.(string)
if raw == "" {
return []string{}, nil
}
return strings.Split(raw, sep), nil
}
}
// StringToTimeDurationHookFunc returns a DecodeHookFunc that converts
// strings to time.Duration.
func StringToTimeDurationHookFunc() DecodeHookFunc {
return func(
f reflect.Type,
t reflect.Type,
data interface{}) (interface{}, error) {
if f.Kind() != reflect.String {
return data, nil
}
if t != reflect.TypeOf(time.Duration(5)) {
return data, nil
}
// Convert it by parsing
return time.ParseDuration(data.(string))
}
}
// StringToIPHookFunc returns a DecodeHookFunc that converts
// strings to net.IP
func StringToIPHookFunc() DecodeHookFunc {
return func(
f reflect.Type,
t reflect.Type,
data interface{}) (interface{}, error) {
if f.Kind() != reflect.String {
return data, nil
}
if t != reflect.TypeOf(net.IP{}) {
return data, nil
}
// Convert it by parsing
ip := net.ParseIP(data.(string))
if ip == nil {
return net.IP{}, fmt.Errorf("failed parsing ip %v", data)
}
return ip, nil
}
}
// StringToIPNetHookFunc returns a DecodeHookFunc that converts
// strings to net.IPNet
func StringToIPNetHookFunc() DecodeHookFunc {
return func(
f reflect.Type,
t reflect.Type,
data interface{}) (interface{}, error) {
if f.Kind() != reflect.String {
return data, nil
}
if t != reflect.TypeOf(net.IPNet{}) {
return data, nil
}
// Convert it by parsing
_, net, err := net.ParseCIDR(data.(string))
return net, err
}
}
// StringToTimeHookFunc returns a DecodeHookFunc that converts
// strings to time.Time.
func StringToTimeHookFunc(layout string) DecodeHookFunc {
return func(
f reflect.Type,
t reflect.Type,
data interface{}) (interface{}, error) {
if f.Kind() != reflect.String {
return data, nil
}
if t != reflect.TypeOf(time.Time{}) {
return data, nil
}
// Convert it by parsing
return time.Parse(layout, data.(string))
}
}
// WeaklyTypedHook is a DecodeHookFunc which adds support for weak typing to
// the decoder.
//
// Note that this is significantly different from the WeaklyTypedInput option
// of the DecoderConfig.
func WeaklyTypedHook(
f reflect.Kind,
t reflect.Kind,
data interface{}) (interface{}, error) {
dataVal := reflect.ValueOf(data)
switch t {
case reflect.String:
switch f {
case reflect.Bool:
if dataVal.Bool() {
return "1", nil
}
return "0", nil
case reflect.Float32:
return strconv.FormatFloat(dataVal.Float(), 'f', -1, 64), nil
case reflect.Int:
return strconv.FormatInt(dataVal.Int(), 10), nil
case reflect.Slice:
dataType := dataVal.Type()
elemKind := dataType.Elem().Kind()
if elemKind == reflect.Uint8 {
return string(dataVal.Interface().([]uint8)), nil
}
case reflect.Uint:
return strconv.FormatUint(dataVal.Uint(), 10), nil
}
}
return data, nil
}
func RecursiveStructToMapHookFunc() DecodeHookFunc {
return func(f reflect.Value, t reflect.Value) (interface{}, error) {
if f.Kind() != reflect.Struct {
return f.Interface(), nil
}
var i interface{} = struct{}{}
if t.Type() != reflect.TypeOf(&i).Elem() {
return f.Interface(), nil
}
m := make(map[string]interface{})
t.Set(reflect.ValueOf(m))
return f.Interface(), nil
}
}
// TextUnmarshallerHookFunc returns a DecodeHookFunc that applies
// strings to the UnmarshalText function, when the target type
// implements the encoding.TextUnmarshaler interface
func TextUnmarshallerHookFunc() DecodeHookFuncType {
return func(
f reflect.Type,
t reflect.Type,
data interface{}) (interface{}, error) {
if f.Kind() != reflect.String {
return data, nil
}
result := reflect.New(t).Interface()
unmarshaller, ok := result.(encoding.TextUnmarshaler)
if !ok {
return data, nil
}
if err := unmarshaller.UnmarshalText([]byte(data.(string))); err != nil {
return nil, err
}
return result, nil
}
}
-50
View File
@@ -1,50 +0,0 @@
package mapstructure
import (
"errors"
"fmt"
"sort"
"strings"
)
// Error implements the error interface and can represents multiple
// errors that occur in the course of a single decode.
type Error struct {
Errors []string
}
func (e *Error) Error() string {
points := make([]string, len(e.Errors))
for i, err := range e.Errors {
points[i] = fmt.Sprintf("* %s", err)
}
sort.Strings(points)
return fmt.Sprintf(
"%d error(s) decoding:\n\n%s",
len(e.Errors), strings.Join(points, "\n"))
}
// WrappedErrors implements the errwrap.Wrapper interface to make this
// return value more useful with the errwrap and go-multierror libraries.
func (e *Error) WrappedErrors() []error {
if e == nil {
return nil
}
result := make([]error, len(e.Errors))
for i, e := range e.Errors {
result[i] = errors.New(e)
}
return result
}
func appendErrors(errors []string, err error) []string {
switch e := err.(type) {
case *Error:
return append(errors, e.Errors...)
default:
return append(errors, e.Error())
}
}
File diff suppressed because it is too large. Load diff
+1
View File
@@ -17,3 +17,4 @@
- Thomas Boerger <thomas@webhippie.de>
- Miroslav Bauer <bauer@cesnet.cz>
- Daniel Mueller <daniel.mueller@uni-muenster.de>
- Michael Stingl <mail@michaelstingl.com>
+1 -1
View File
@@ -27,7 +27,7 @@ import (
"strconv"
"strings"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/opencloud-eu/reva/v2/cmd/revad/internal/grace"
"github.com/opencloud-eu/reva/v2/pkg/logger"
"github.com/opencloud-eu/reva/v2/pkg/registry"
@@ -27,7 +27,7 @@ import (
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/auth/scope"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
@@ -28,7 +28,7 @@ import (
collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/events"
@@ -22,7 +22,7 @@ import (
"context"
appauthpb "github.com/cs3org/go-cs3apis/cs3/auth/applications/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/opencloud-eu/reva/v2/pkg/appauth"
"github.com/opencloud-eu/reva/v2/pkg/appauth/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
@@ -28,8 +28,8 @@ import (
registrypb "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/go-viper/mapstructure/v2"
"github.com/juliangruber/go-intersect"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/app"
"github.com/opencloud-eu/reva/v2/pkg/app/provider/registry"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
@@ -24,7 +24,7 @@ import (
"google.golang.org/grpc"
registrypb "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/opencloud-eu/reva/v2/pkg/app"
"github.com/opencloud-eu/reva/v2/pkg/app/registry/registry"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
@@ -24,7 +24,7 @@ import (
"path/filepath"
provider "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/auth"
"github.com/opencloud-eu/reva/v2/pkg/auth/manager/registry"
@@ -22,7 +22,7 @@ import (
"context"
registrypb "github.com/cs3org/go-cs3apis/cs3/auth/registry/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/opencloud-eu/reva/v2/pkg/auth"
"github.com/opencloud-eu/reva/v2/pkg/auth/registry/registry"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
@@ -30,7 +30,7 @@ import (
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
datatx "github.com/cs3org/go-cs3apis/cs3/tx/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
txdriver "github.com/opencloud-eu/reva/v2/pkg/datatx"
txregistry "github.com/opencloud-eu/reva/v2/pkg/datatx/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
@@ -24,7 +24,7 @@ import (
"strings"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
@@ -24,7 +24,7 @@ import (
"sort"
grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/group"
"github.com/opencloud-eu/reva/v2/pkg/group/manager/registry"
@@ -22,7 +22,7 @@ import (
"context"
"fmt"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/opencloud-eu/reva/v2/internal/grpc/services/helloworld/proto"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/pkg/errors"
@@ -22,7 +22,7 @@ import (
"context"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/ocm/provider"
"github.com/opencloud-eu/reva/v2/pkg/ocm/provider/authorizer/registry"
@@ -24,7 +24,7 @@ import (
permissions "github.com/cs3org/go-cs3apis/cs3/permissions/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/opencloud-eu/reva/v2/pkg/permission"
"github.com/opencloud-eu/reva/v2/pkg/permission/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
@@ -24,7 +24,7 @@ import (
"google.golang.org/grpc"
preferencespb "github.com/cs3org/go-cs3apis/cs3/preferences/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/preferences"
"github.com/opencloud-eu/reva/v2/pkg/preferences/registry"
@@ -30,7 +30,7 @@ import (
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/opencloud-eu/reva/v2/pkg/password"
"github.com/opencloud-eu/reva/v2/pkg/permission"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
@@ -33,7 +33,7 @@ import (
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
@@ -39,7 +39,7 @@ import (
collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
@@ -33,7 +33,7 @@ import (
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/conversions"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
@@ -28,7 +28,7 @@ import (
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
registrypb "github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1"
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
@@ -24,7 +24,7 @@ import (
"path/filepath"
"sort"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"google.golang.org/grpc"
@@ -33,7 +33,7 @@ import (
collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"google.golang.org/grpc"
@@ -31,7 +31,7 @@ import (
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/opencloud-eu/reva/v2/internal/http/interceptors/auth/credential/registry"
tokenregistry "github.com/opencloud-eu/reva/v2/internal/http/interceptors/auth/token/registry"
tokenwriterregistry "github.com/opencloud-eu/reva/v2/internal/http/interceptors/auth/tokenwriter/registry"
@@ -19,7 +19,7 @@
package cors
import (
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/global"
"github.com/rs/cors"
)
@@ -25,7 +25,7 @@ import (
"strings"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/ocm/provider"
@@ -33,8 +33,8 @@ import (
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
iso6391 "github.com/emvi/iso-639-1"
"github.com/go-chi/chi/v5"
"github.com/go-viper/mapstructure/v2"
ua "github.com/mileusna/useragent"
"github.com/mitchellh/mapstructure"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
@@ -32,7 +32,7 @@ import (
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/gdexlab/go-render/render"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/opencloud-eu/reva/v2/internal/http/services/archiver/manager"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
@@ -27,8 +27,8 @@ import (
"strconv"
"time"
"github.com/go-viper/mapstructure/v2"
"github.com/golang-jwt/jwt/v5"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rhttp"
@@ -22,7 +22,7 @@ import (
"fmt"
"net/http"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/rs/zerolog"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
@@ -21,7 +21,7 @@ package helloworld
import (
"net/http"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/global"
"github.com/rs/zerolog"
@@ -21,7 +21,7 @@ package mentix
import (
"net/http"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/opencloud-eu/reva/v2/pkg/mentix/meshdata"
"github.com/pkg/errors"
"github.com/rs/zerolog"
@@ -25,7 +25,7 @@ import (
"net/http"
"os"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/opencloud-eu/reva/v2/pkg/logger"
"github.com/rs/zerolog"
@@ -159,6 +159,8 @@ var (
ErrLocked = errors.New("webdav: locked")
// ErrNoSuchLock is returned by a LockSystem's Refresh and Unlock methods.
ErrNoSuchLock = errors.New("webdav: no such lock")
// ErrNotFound is returned when a resource does not exist.
ErrNotFound = errors.New("webdav: not found")
// ErrNotImplemented is returned when hitting not implemented code paths
ErrNotImplemented = errors.New("webdav: not implemented")
// ErrTokenNotFound is returned when a token is not found
@@ -206,6 +208,8 @@ func NewErrFromStatus(s *rpc.Status) error {
return ErrForbidden
case rpc.Code_CODE_LOCKED, rpc.Code_CODE_FAILED_PRECONDITION:
return ErrLocked
case rpc.Code_CODE_NOT_FOUND:
return ErrNotFound
case rpc.Code_CODE_UNIMPLEMENTED:
return ErrNotImplemented
default:
@@ -546,20 +546,26 @@ func (s *svc) lockReference(ctx context.Context, w http.ResponseWriter, r *http.
ld.OwnerXML = li.Owner.InnerXML // TODO optional, should be a URL
ld.ZeroDepth = depth == 0
//TODO: @jfd the code tries to create a lock for a file that may not even exist,
// should we do that in the decomposedfs as well? the node does not exist
// this actually is a name based lock ... ugh
token, err = s.LockSystem.Create(ctx, now, ld)
//
if err != nil {
switch {
case errors.Is(err, ocdavErrors.ErrLocked):
return http.StatusLocked, err
case errors.Is(err, ocdavErrors.ErrForbidden):
return http.StatusForbidden, err
case errors.Is(err, ocdavErrors.ErrNotFound):
// RFC 4918 section 7.3: a LOCK request on an unmapped URL
// creates an empty locked resource. Create the resource, then
// retry the lock against the now-existing node.
resourceCreated, status, cerr := s.createEmptyResource(ctx, ref)
if cerr != nil {
return status, cerr
}
// Report 201 only when the resource was actually created; a
// concurrent create leaves an existing resource to lock (200).
created = resourceCreated
if token, err = s.LockSystem.Create(ctx, now, ld); err != nil {
sublog.Error().Err(err).Msg("could not lock resource after creating it")
return lockCreateErrToStatus(err), err
}
default:
return http.StatusInternalServerError, err
return lockCreateErrToStatus(err), err
}
}
@@ -571,19 +577,9 @@ func (s *svc) lockReference(ctx context.Context, w http.ResponseWriter, r *http.
}
}()
// Create the resource if it didn't previously exist.
// TODO use sdk to stat?
/*
if _, err := s.FileSystem.Stat(ctx, reqPath); err != nil {
f, err := s.FileSystem.OpenFile(ctx, reqPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
// TODO: detect missing intermediate dirs and return http.StatusConflict?
return http.StatusInternalServerError, err
}
f.Close()
created = true
}
*/
// The resource is created (if it did not previously exist) in the
// ErrNotFound branch above, mirroring sabre/dav per RFC 4918 section 7.3.
// http://www.webdav.org/specs/rfc4918.html#HEADER_Lock-Token says that the
// Lock-Token value is a Coded-URL. We add angle brackets.
w.Header().Set("Lock-Token", "<"+token+">")
@@ -603,6 +599,59 @@ func (s *svc) lockReference(ctx context.Context, w http.ResponseWriter, r *http.
return 0, nil
}
// lockCreateErrToStatus maps a LockSystem.Create error to the HTTP status the
// LOCK handler returns. ErrNotFound is handled separately by the caller (it
// creates the resource and retries), so it falls through to 500 here.
func lockCreateErrToStatus(err error) int {
switch {
case errors.Is(err, ocdavErrors.ErrLocked):
return http.StatusLocked
case errors.Is(err, ocdavErrors.ErrForbidden):
return http.StatusForbidden
default:
return http.StatusInternalServerError
}
}
// createEmptyResource creates a 0-byte file at ref so that a LOCK on an
// unmapped URL can succeed, as required by RFC 4918 section 7.3. It mirrors
// the empty-file path of handlePut, which uses TouchFile for a zero-length
// upload. The first return value reports whether the resource was actually
// created (CODE_OK); on a concurrent create (CODE_ALREADY_EXISTS) it is false
// so the caller locks the existing resource and returns 200 rather than 201.
// On failure it returns an (http status, error) pair the LOCK handler can
// return directly. Server-side failures (500) are logged, mirroring handlePut.
func (s *svc) createEmptyResource(ctx context.Context, ref *provider.Reference) (bool, int, error) {
client, err := s.gatewaySelector.Next()
if err != nil {
appctx.GetLogger(ctx).Error().Err(err).Msg("error selecting next gateway client for lock")
return false, http.StatusInternalServerError, err
}
res, err := client.TouchFile(ctx, &provider.TouchFileRequest{Ref: ref})
if err != nil {
appctx.GetLogger(ctx).Error().Err(err).Interface("ref", ref).Msg("error sending grpc touch file request on lock")
return false, http.StatusInternalServerError, err
}
switch res.GetStatus().GetCode() {
case rpc.Code_CODE_OK:
return true, 0, nil
case rpc.Code_CODE_ALREADY_EXISTS:
// The resource appeared concurrently. Lock the existing one (200).
return false, 0, nil
case rpc.Code_CODE_NOT_FOUND:
// A missing intermediate collection. RFC 4918 section 9.10.6 maps this
// to 409 Conflict, not 500.
return false, http.StatusConflict, ocdavErrors.NewErrFromStatus(res.GetStatus())
case rpc.Code_CODE_PERMISSION_DENIED:
return false, http.StatusForbidden, ocdavErrors.NewErrFromStatus(res.GetStatus())
default:
appctx.GetLogger(ctx).Error().Interface("status", res.GetStatus()).Interface("ref", ref).Msg("error touching file on lock")
return false, http.StatusInternalServerError, ocdavErrors.NewErrFromStatus(res.GetStatus())
}
}
func writeLockInfo(w io.Writer, token string, ld LockDetails) (int, error) {
depth := "infinity"
if ld.ZeroDepth {
@@ -31,8 +31,8 @@ import (
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/go-viper/mapstructure/v2"
"github.com/jellydator/ttlcache/v2"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/config"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
@@ -1736,12 +1736,11 @@ func hasPreview(md *provider.ResourceInfo, appendToOK func(p ...prop.PropertyXML
}
func downloadURL(ctx context.Context, log zerolog.Logger, isPublic bool, path string, ls *link.PublicShare, publicURL string, baseURI string, urlSigner signedurl.Signer) string {
parts := strings.Split(path, "/")
encodedPath, err := url.JoinPath("/", parts...)
if err != nil {
log.Error().Err(err).Msg("failed to encode the path for the download URL")
return ""
}
// Encode the path with net.EncodePath, the same encoder the resource href
// uses, so a filename with a literal "%" or "#" round-trips. url.JoinPath
// treats a literal "%" as an escape, so such names 404.
// See https://github.com/opencloud-eu/opencloud/issues/2852.
encodedPath := net.EncodePath(path)
switch {
case isPublic:
@@ -22,8 +22,8 @@ import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-viper/mapstructure/v2"
"github.com/jellydator/ttlcache/v2"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/config"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/handlers/apps/sharing/sharees"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares"
@@ -25,7 +25,7 @@ import (
preferences "github.com/cs3org/go-cs3apis/cs3/preferences/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
"github.com/go-chi/chi/v5"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/global"
@@ -22,7 +22,7 @@ import (
"net/http"
"contrib.go.opencensus.io/exporter/prometheus"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"go.opencensus.io/stats/view"
@@ -26,7 +26,7 @@ import (
"os"
"github.com/go-chi/chi/v5"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/global"
"github.com/rs/zerolog"
@@ -21,7 +21,7 @@ package siteacc
import (
"net/http"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/opencloud-eu/reva/v2/pkg/siteacc"
"github.com/opencloud-eu/reva/v2/pkg/siteacc/config"
"github.com/pkg/errors"
@@ -21,7 +21,7 @@ package sysinfo
import (
"net/http"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/pkg/errors"
"github.com/rs/zerolog"
+1 -1
View File
@@ -25,7 +25,7 @@ import (
appprovider "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
appregistry "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/opencloud-eu/reva/v2/pkg/app"
"github.com/opencloud-eu/reva/v2/pkg/app/provider/registry"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
+1 -1
View File
@@ -37,8 +37,8 @@ import (
appregistry "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/go-viper/mapstructure/v2"
"github.com/golang-jwt/jwt/v5"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/app"
"github.com/opencloud-eu/reva/v2/pkg/app/provider/registry"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
+1 -1
View File
@@ -18,7 +18,7 @@
package micro
import "github.com/mitchellh/mapstructure"
import "github.com/go-viper/mapstructure/v2"
type config struct {
Namespace string `mapstructure:"namespace"`
+1 -1
View File
@@ -27,7 +27,7 @@ import (
"sync"
registrypb "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/opencloud-eu/reva/v2/pkg/app"
"github.com/opencloud-eu/reva/v2/pkg/app/registry/registry"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
+1 -1
View File
@@ -30,7 +30,7 @@ import (
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/opencloud-eu/reva/v2/pkg/appauth"
"github.com/opencloud-eu/reva/v2/pkg/appauth/manager/registry"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
+61 -192
View File
@@ -2,9 +2,7 @@ package jsoncs3
import (
"context"
"encoding/json"
"fmt"
"math/rand"
"strings"
"sync"
"time"
@@ -14,19 +12,21 @@ import (
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/go-viper/mapstructure/v2"
"github.com/google/uuid"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/appauth"
"github.com/opencloud-eu/reva/v2/pkg/appauth/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/metadatacache"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/pkg/errors"
"github.com/sethvargo/go-diceware/diceware"
"github.com/sethvargo/go-password/password"
"go.opentelemetry.io/otel/codes"
"google.golang.org/protobuf/proto"
)
type PasswordGenerator interface {
@@ -40,9 +40,9 @@ func init() {
type manager struct {
sync.RWMutex // for lazy initialization
mds metadata.Storage
store *metadatacache.Store[string, map[string]*apppb.AppPassword]
generator PasswordGenerator
uTimeUpdateInterval time.Duration
updateRetryCount int
initialized bool
}
@@ -59,8 +59,6 @@ type config struct {
UpdateRetryCount int `mapstructure:"update_retry_count"`
}
type updaterFunc func(map[string]*apppb.AppPassword) (map[string]*apppb.AppPassword, error)
const tracerName = "jsoncs3"
func New(m map[string]any) (appauth.Manager, error) {
@@ -127,11 +125,17 @@ func New(m map[string]any) (appauth.Manager, error) {
}
func NewWithOptions(mds metadata.Storage, generator PasswordGenerator, uTimeUpdateInterval time.Duration, updateRetries int) (*manager, error) {
store := metadatacache.New(metadatacache.Options[string, map[string]*apppb.AppPassword]{
Storage: mds,
Path: func(userID string) string { return userID + ".json" },
Retries: updateRetries,
Init: func() map[string]*apppb.AppPassword { return map[string]*apppb.AppPassword{} },
})
return &manager{
mds: mds,
store: store,
generator: generator,
uTimeUpdateInterval: uTimeUpdateInterval,
updateRetryCount: updateRetries,
}, nil
}
@@ -170,7 +174,7 @@ func (m *manager) GenerateAppPassword(ctx context.Context, scope map[string]*aut
cTime := &typespb.Timestamp{Seconds: uint64(time.Now().Unix())}
// For persisting we use the hashed password, since we don't
// want to store it in cleartext
// want to store it in cleartext.
appPass := &apppb.AppPassword{
Password: tokenHashed,
TokenScope: scope,
@@ -183,21 +187,26 @@ func (m *manager) GenerateAppPassword(ctx context.Context, scope map[string]*aut
id := uuid.New().String()
err = m.updateWithRetry(ctx, m.updateRetryCount, true, userID, func(a map[string]*apppb.AppPassword) (map[string]*apppb.AppPassword, error) {
err = m.store.Update(ctx, userID.GetOpaqueId(), true, func(a map[string]*apppb.AppPassword) (map[string]*apppb.AppPassword, bool, error) {
a[id] = appPass
return a, nil
return a, true, nil
})
if err != nil {
logger.Debug().Err(err).Msg("failed to store new app password")
return nil, err
}
// Here we need to resplace the hash with the cleartext password again since
// the requestor needs to know the cleartext value.
appPass.Password = token
return appPass, nil
// Return a fresh AppPassword with the cleartext token for the caller.
// Constructing from scratch avoids copying the proto struct (which contains a mutex).
return &apppb.AppPassword{
Password: token,
TokenScope: scope,
Label: label,
Expiration: expiration,
Ctime: cTime,
Utime: cTime,
User: userID,
}, nil
}
// ListAppPasswords lists the application passwords created by a user.
@@ -216,23 +225,26 @@ func (m *manager) ListAppPasswords(ctx context.Context) ([]*apppb.AppPassword, e
} else {
return nil, errtypes.BadRequest("no user in context")
}
_, userAppPasswords, err := m.getUserAppPasswords(ctx, userID)
unlock := m.store.Lock(userID.GetOpaqueId())
defer unlock()
passwords, ok, err := m.store.Get(ctx, userID.GetOpaqueId())
if err != nil {
if _, ok := err.(errtypes.NotFound); ok {
return []*apppb.AppPassword{}, nil
}
log.Error().Err(err).Msg("getUserAppPasswords failed")
log.Error().Err(err).Msg("store.Get failed")
return nil, err
}
userAppPasswordSlice := make([]*apppb.AppPassword, 0, len(userAppPasswords))
for id, p := range userAppPasswords {
p.Password = id
userAppPasswordSlice = append(userAppPasswordSlice, p)
if !ok {
return []*apppb.AppPassword{}, nil
}
return userAppPasswordSlice, nil
result := make([]*apppb.AppPassword, 0, len(passwords))
for id, p := range passwords {
pw := proto.Clone(p).(*apppb.AppPassword)
pw.Password = id
result = append(result, pw)
}
return result, nil
}
// InvalidateAppPassword invalidates a generated password.
@@ -253,16 +265,16 @@ func (m *manager) InvalidateAppPassword(ctx context.Context, secretOrId string)
return errtypes.BadRequest("no user in context")
}
updater := func(a map[string]*apppb.AppPassword) (map[string]*apppb.AppPassword, error) {
err := m.store.Update(ctx, userID.GetOpaqueId(), false, func(a map[string]*apppb.AppPassword) (map[string]*apppb.AppPassword, bool, error) {
// Allow deleting a token using the ID inside the password property. This is needed because of
// some shortcomings of the CS3 APIs. On the API level tokens don't have IDs
// some shortcomings of the CS3 APIs. On the API level tokens don't have IDs;
// ListAppPasswords in this backend returns the ID as the password value.
if _, ok := a[secretOrId]; ok {
delete(a, secretOrId)
return a, nil
return a, true, nil
}
// Check if the supplied parameter matches any of the stored password tokens
// Check if the supplied parameter matches any of the stored password tokens.
for key, pw := range a {
ok, err := argon2id.ComparePasswordAndHash(secretOrId, pw.Password)
switch {
@@ -270,15 +282,13 @@ func (m *manager) InvalidateAppPassword(ctx context.Context, secretOrId string)
log.Debug().Err(err).Msg("Error comparing password and hash")
case ok:
delete(a, key)
return a, nil
return a, true, nil
}
}
return a, errtypes.NotFound("password not found")
}
err := m.updateWithRetry(ctx, m.updateRetryCount, false, userID, updater)
return a, false, errtypes.NotFound("password not found")
})
if err != nil {
log.Error().Err(err).Msg("getUserAppPasswords failed")
log.Error().Err(err).Msg("store.Update failed")
return errtypes.NotFound("password not found")
}
return nil
@@ -295,13 +305,12 @@ func (m *manager) GetAppPassword(ctx context.Context, user *userpb.UserId, secre
return nil, err
}
errUpdateSkipped := errors.New("update skipped")
var (
matchedPw *apppb.AppPassword
matchedID string
)
updater := func(a map[string]*apppb.AppPassword) (map[string]*apppb.AppPassword, error) {
err := m.store.Update(ctx, user.GetOpaqueId(), false, func(a map[string]*apppb.AppPassword) (map[string]*apppb.AppPassword, bool, error) {
matchedPw = nil
for id, pw := range a {
ok, err := argon2id.ComparePasswordAndHash(secret, pw.Password)
@@ -312,35 +321,31 @@ func (m *manager) GetAppPassword(ctx context.Context, user *userpb.UserId, secre
// password found
if pw.Expiration != nil && pw.Expiration.Seconds != 0 && uint64(time.Now().Unix()) > pw.Expiration.Seconds {
log.Debug().Str("AppPasswordId", id).Msg("password expired")
return nil, errtypes.NotFound("password not found")
return nil, false, errtypes.NotFound("password not found")
}
matchedPw = pw
matchedID = id
// password not expired
// Updating the Utime will cause an Upload for every single GetAppPassword request. We are limiting this to one
// update per 'uTimeUpdateInterval' (default 5 min) otherwise this backend will become unusable.
if time.Since(utils.TSToTime(pw.Utime)) > m.uTimeUpdateInterval {
a[id].Utime = utils.TSNow()
return a, nil
return a, true, nil
}
return a, errUpdateSkipped
return a, false, nil
}
}
return nil, false, errtypes.NotFound("password not found")
})
if err != nil {
return nil, errtypes.NotFound("password not found")
}
err := m.updateWithRetry(ctx, m.updateRetryCount, false, user, updater)
switch {
case err == nil:
fallthrough
case errors.Is(err, errUpdateSkipped):
// Don't return the hashed password, put the ID into the password field
matchedPw.Password = matchedID
return matchedPw, nil
}
return nil, errtypes.NotFound("password not found")
// Return a clone with the ID in the password field so the cached entry
// is not corrupted.
result := proto.Clone(matchedPw).(*apppb.AppPassword)
result.Password = matchedID
return result, nil
}
func (m *manager) initialize(ctx context.Context) error {
@@ -372,142 +377,6 @@ func (m *manager) initialize(ctx context.Context) error {
return nil
}
func (m *manager) updateWithRetry(ctx context.Context, retries int, createIfNotFound bool, userid *userpb.UserId, updater updaterFunc) error {
log := appctx.GetLogger(ctx)
_, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "initialize")
defer span.End()
retry := true
var (
etag string
userAppPasswords map[string]*apppb.AppPassword
err error
)
// retry for the specified number of times, then error out
for i := 0; i < retries && retry; i++ {
if i > 0 {
// if we're retrying, wait a bit before the next try
jitter := time.Duration(rand.Int63n(int64(100 * time.Millisecond)))
time.Sleep(jitter + 100*time.Millisecond)
}
etag, userAppPasswords, err = m.getUserAppPasswords(ctx, userid)
switch err.(type) {
case nil:
// empty
case errtypes.NotFound:
if createIfNotFound {
userAppPasswords = map[string]*apppb.AppPassword{}
} else {
log.Debug().Err(err).Msg("getUserAppPasswords failed (not found)")
span.RecordError(err)
span.SetStatus(codes.Error, "downloading app tokens failed")
return err
}
case errtypes.TooEarly:
// Ideally this should never happen as we disable asynchronous uploads for the metadata storage.
log.Debug().Err(err).Int("try", i).Msg("getUserAppPasswords failed (too early, retrying)")
retry = true
continue
default:
log.Debug().Err(err).Msg("getUserAppPasswords failed")
span.RecordError(err)
span.SetStatus(codes.Error, "downloading app tokens failed")
return err
}
userAppPasswords, err = updater(userAppPasswords)
if err != nil {
return err
}
err = m.updateUserAppPassword(ctx, userid, userAppPasswords, etag)
switch err.(type) {
case nil:
retry = false
case errtypes.Aborted:
log.Debug().Err(err).Int("attempt", i).Msg("updateUserAppPassword failed (retrying)")
retry = true
default:
log.Debug().Err(err).Int("attempt", i).Msg("updateUserAppPassword failed (not retrying)")
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
}
}
if retry {
log.Debug().Err(err).Msg("updateUserAppPassword failed")
span.RecordError(err)
span.SetStatus(codes.Error, "updating app token failed")
return err
}
return nil
}
func (m *manager) updateUserAppPassword(ctx context.Context, userid *userpb.UserId, appPasswords map[string]*apppb.AppPassword, ifMatchEtag string) error {
log := appctx.GetLogger(ctx)
ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "getUserAppPasswords")
jsonPath := userAppTokenJSONPath(userid)
pwBytes, err := json.Marshal(appPasswords)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
}
ur := metadata.UploadRequest{
Path: jsonPath,
Content: pwBytes,
IfMatchEtag: ifMatchEtag,
}
// If there is no etag, make sure to only upload if the file wasn't craeted yet
if ifMatchEtag == "" {
ur.IfNoneMatch = []string{"*"}
}
_, err = m.mds.Upload(ctx, ur)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
log.Debug().Err(err).Msg("failed to upload AppPasswword")
return err
}
return nil
}
func (m *manager) getUserAppPasswords(ctx context.Context, userid *userpb.UserId) (string, map[string]*apppb.AppPassword, error) {
log := appctx.GetLogger(ctx)
ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "getUserAppPasswords")
jsonPath := userAppTokenJSONPath(userid)
dlreq := metadata.DownloadRequest{
Path: jsonPath,
}
var userAppPasswords = map[string]*apppb.AppPassword{}
dlres, err := m.mds.Download(ctx, dlreq)
switch err.(type) {
case nil:
err = json.Unmarshal(dlres.Content, &userAppPasswords)
if err != nil {
log.Error().Err(err).Msg("unmarshaling app tokens failed")
return "", nil, err
}
case errtypes.NotFound:
return "", nil, errtypes.NotFound("password not found")
default:
span.RecordError(err)
span.SetStatus(codes.Error, "downloading app tokens failed")
return "", nil, err
}
return dlres.Etag, userAppPasswords, nil
}
func userAppTokenJSONPath(userID *userpb.UserId) string {
return userID.GetOpaqueId() + ".json"
}
type randomPassword struct {
Strength int `mapstructure:"token_strength"`
}
@@ -25,7 +25,7 @@ import (
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/opencloud-eu/reva/v2/pkg/auth"
"github.com/opencloud-eu/reva/v2/pkg/auth/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
+1 -1
View File
@@ -26,7 +26,7 @@ import (
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/opencloud-eu/reva/v2/pkg/auth"
"github.com/opencloud-eu/reva/v2/pkg/auth/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/auth/scope"
+1 -1
View File
@@ -27,8 +27,8 @@ import (
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
"github.com/go-ldap/ldap/v3"
"github.com/go-viper/mapstructure/v2"
"github.com/google/uuid"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/auth"
"github.com/opencloud-eu/reva/v2/pkg/auth/manager/registry"
@@ -25,7 +25,7 @@ import (
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/opencloud-eu/reva/v2/pkg/auth"
"github.com/opencloud-eu/reva/v2/pkg/auth/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/auth/scope"
+1 -1
View File
@@ -32,8 +32,8 @@ import (
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
"github.com/go-viper/mapstructure/v2"
"github.com/juliangruber/go-intersect"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/auth"
"github.com/opencloud-eu/reva/v2/pkg/auth/manager/registry"
@@ -28,7 +28,7 @@ import (
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/opencloud-eu/reva/v2/pkg/auth"
"github.com/opencloud-eu/reva/v2/pkg/auth/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/auth/scope"
@@ -7,7 +7,7 @@ import (
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/rs/zerolog"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/opencloud-eu/reva/v2/pkg/auth"
"github.com/opencloud-eu/reva/v2/pkg/auth/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/auth/scope"
+1 -1
View File
@@ -22,7 +22,7 @@ import (
"context"
registrypb "github.com/cs3org/go-cs3apis/cs3/auth/registry/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/opencloud-eu/reva/v2/pkg/auth"
"github.com/opencloud-eu/reva/v2/pkg/auth/registry/registry"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
@@ -34,8 +34,8 @@ import (
datatx "github.com/cs3org/go-cs3apis/cs3/tx/v1beta1"
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/go-viper/mapstructure/v2"
"github.com/google/uuid"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
txdriver "github.com/opencloud-eu/reva/v2/pkg/datatx"
registry "github.com/opencloud-eu/reva/v2/pkg/datatx/manager/registry"
+1 -1
View File
@@ -27,7 +27,7 @@ import (
grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/group"
"github.com/opencloud-eu/reva/v2/pkg/group/manager/registry"
+1 -1
View File
@@ -27,8 +27,8 @@ import (
grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/go-ldap/ldap/v3"
"github.com/go-viper/mapstructure/v2"
"github.com/google/uuid"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/group"
+1 -1
View File
@@ -24,7 +24,7 @@ import (
"os"
"time"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/pkg/errors"
"github.com/rs/zerolog"
)
@@ -39,7 +39,7 @@ import (
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/go-viper/mapstructure/v2"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/publicshare"
Loaded 100 of 146 files, more files were not shown because too many files have changed in this diff. Show more