diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9784aeb --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +test.db +*.mercury +sour.is-mercury \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..1e1cc2b --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,23 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${fileDirname}", + "cwd": "${workspaceFolder}" + }, + { + "name": "Attach to Process", + "type": "go", + "request": "attach", + "mode": "local", + "processId": 0 + } + ] +} \ No newline at end of file diff --git a/cmd/testsql/main.go b/cmd/testsql/main.go new file mode 100644 index 0000000..9b21798 --- /dev/null +++ b/cmd/testsql/main.go @@ -0,0 +1,45 @@ +package main + +import ( + "database/sql" + "log" + + _ "modernc.org/sqlite" +) + +func main() { + db, err := sql.Open("sqlite", "./test.db") + if err != nil { + log.Fatal(err) + } + defer db.Close() + + _, err = db.Exec(`drop table if exists foo`) + if err != nil { + log.Fatal(err) + } + + _, err = db.Exec(`create table foo (bar jsonb)`) + if err != nil { + log.Fatal(err) + } + + _, err = db.Exec(`insert into foo (bar) values ('["one"]')`) + if err != nil { + log.Fatal(err) + } + + rows, err := db.Query(`select j.value from foo, json_each(bar) j `) + if err != nil { + log.Fatal(err) + } + for rows.Next() { + var s string + err = rows.Scan(&s) + if err != nil { + log.Fatal(err) + } + + log.Println("GOT: ", s) + } +} \ No newline at end of file diff --git a/cron/cron.go b/cron/cron.go index e912fc8..43414c4 100644 --- a/cron/cron.go +++ b/cron/cron.go @@ -127,9 +127,6 @@ func (c *cron) run(ctx context.Context, now time.Time) { case <-timer.C: } - span.AddEvent("Cron Run: " + now.Format(time.RFC822)) - // fmt.Println("Cron Run: ", now.Format(time.RFC822)) - c.state.Use(ctx, func(ctx context.Context, state *state) error { run = append(run, state.queue...) state.queue = state.queue[:0] @@ -148,6 +145,9 @@ func (c *cron) run(ctx context.Context, now time.Time) { return } + span.AddEvent("Cron Run: " + now.Format(time.RFC822)) + // fmt.Println("Cron Run: ", now.Format(time.RFC822)) + wg, _ := errgroup.WithContext(ctx) for i := range run { diff --git a/env/env.go b/env/env.go index 0f52570..460dc26 100644 --- a/env/env.go +++ b/env/env.go @@ -9,35 +9,16 @@ import ( "strings" ) -func Default(name, defaultValue string) string { +func Default(name, defaultValue string) (s string) { name = strings.TrimSpace(name) - defaultValue = strings.TrimSpace(defaultValue) - if v := strings.TrimSpace(os.Getenv(name)); v != "" { - slog.Info("env", name, v) - return v - } - slog.Info("env", name, defaultValue+" (default)") - return defaultValue -} + s = strings.TrimSpace(defaultValue) -type secret string + if v, ok := os.LookupEnv(name); ok { + s = strings.TrimSpace(v) + slog.Info("env", slog.String(name, v)) + return + } -func (s secret) String() string { - if s == "" { - return "(nil)" - } - return "***" -} -func (s secret) Secret() string { - return string(s) -} -func Secret(name, defaultValue string) secret { - name = strings.TrimSpace(name) - defaultValue = strings.TrimSpace(defaultValue) - if v := strings.TrimSpace(os.Getenv(name)); v != "" { - slog.Info("env", name, secret(v)) - return secret(v) - } - slog.Info("env", name, secret(defaultValue).String()+" (default)") - return secret(defaultValue) + slog.Info("env", slog.String(name, s+" (default)")) + return } diff --git a/env/secret.go b/env/secret.go new file mode 100644 index 0000000..ac7bc98 --- /dev/null +++ b/env/secret.go @@ -0,0 +1,35 @@ +package env + +import ( + "os" + "log/slog" + "strings" +) + +type secret string + +func (s secret) String() string { + if s == "" { + return "(nil)" + } + return "***" +} + +func (s secret) Secret() string { + return string(s) +} + +func Secret(name, defaultValue string) (s secret) { + name = strings.TrimSpace(name) + s = secret(strings.TrimSpace(defaultValue)) + + if v, ok := os.LookupEnv(name); ok { + s = secret(strings.TrimSpace(v)) + slog.Info("env", slog.String(name, s.String())) + return + } + + slog.Info("env", slog.String(name, s.String()+" (default)")) + return +} + diff --git a/go.mod b/go.mod index 85d54e1..5e6bbbe 100644 --- a/go.mod +++ b/go.mod @@ -1,55 +1,77 @@ module go.sour.is/pkg -go 1.21 +go 1.22.0 require ( - github.com/99designs/gqlgen v0.17.34 + github.com/99designs/gqlgen v0.17.44 github.com/golang-jwt/jwt/v4 v4.5.0 - github.com/gorilla/websocket v1.5.0 + github.com/gorilla/websocket v1.5.1 github.com/matryer/is v1.4.1 - github.com/ravilushqa/otelgqlgen v0.13.1 - github.com/vektah/gqlparser/v2 v2.5.6 - go.opentelemetry.io/otel v1.18.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.18.0 - go.opentelemetry.io/otel/sdk/metric v0.41.0 + github.com/ravilushqa/otelgqlgen v0.15.0 + github.com/vektah/gqlparser/v2 v2.5.11 + go.opentelemetry.io/otel v1.23.1 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.23.1 + go.opentelemetry.io/otel/sdk/metric v1.23.1 + go.sour.is/passwd v0.2.0 go.uber.org/multierr v1.11.0 - golang.org/x/sync v0.3.0 + golang.org/x/sync v0.6.0 ) require ( github.com/agnivade/levenshtein v1.1.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/felixge/httpsnoop v1.0.3 // indirect - github.com/hashicorp/golang-lru/v2 v2.0.3 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect + github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect - github.com/prometheus/common v0.44.0 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.47.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect - go.opentelemetry.io/contrib v1.16.1 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/sosodev/duration v1.2.0 // indirect + go.opentelemetry.io/contrib v1.23.0 // indirect + google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240213162025-012b6fc9bca9 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240213162025-012b6fc9bca9 // indirect + modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect + modernc.org/libc v1.41.0 // indirect + modernc.org/mathutil v1.6.0 // indirect + modernc.org/memory v1.7.2 // indirect + modernc.org/strutil v1.2.0 // indirect + modernc.org/token v1.1.0 // indirect ) require ( + github.com/BurntSushi/toml v1.3.2 + github.com/Masterminds/squirrel v1.5.4 github.com/cenkalti/backoff/v4 v4.2.1 // indirect - github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f github.com/golang/protobuf v1.5.3 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect - github.com/prometheus/client_golang v1.17.0 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.44.0 - go.opentelemetry.io/contrib/instrumentation/runtime v0.42.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.18.0 // indirect - go.opentelemetry.io/otel/exporters/prometheus v0.41.0 - go.opentelemetry.io/otel/metric v1.18.0 - go.opentelemetry.io/otel/sdk v1.18.0 - go.opentelemetry.io/otel/trace v1.18.0 - go.opentelemetry.io/proto/otlp v1.0.0 // indirect - golang.org/x/net v0.17.0 // indirect - golang.org/x/sys v0.13.0 // indirect - golang.org/x/text v0.13.0 // indirect - google.golang.org/grpc v1.58.0 // indirect - google.golang.org/protobuf v1.31.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 // indirect + github.com/oklog/ulid/v2 v2.1.0 + github.com/prometheus/client_golang v1.18.0 + go.nhat.io/otelsql v0.12.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0 + go.opentelemetry.io/contrib/instrumentation/runtime v0.48.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.23.1 // indirect + go.opentelemetry.io/otel/exporters/prometheus v0.45.2 + go.opentelemetry.io/otel/metric v1.23.1 + go.opentelemetry.io/otel/sdk v1.23.1 + go.opentelemetry.io/otel/trace v1.23.1 + go.opentelemetry.io/proto/otlp v1.1.0 // indirect + golang.org/x/exp v0.0.0-20240213143201-ec583247a57a + golang.org/x/net v0.21.0 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/grpc v1.61.1 // indirect + google.golang.org/protobuf v1.32.0 // indirect + modernc.org/sqlite v1.29.1 ) diff --git a/go.sum b/go.sum index 3609787..258778f 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,13 @@ -github.com/99designs/gqlgen v0.17.34 h1:5cS5/OKFguQt+Ws56uj9FlG2xm1IlcJWNF2jrMIKYFQ= -github.com/99designs/gqlgen v0.17.34/go.mod h1:Axcd3jIFHBVcqzixujJQr1wGqE+lGTpz6u4iZBZg1G8= +cloud.google.com/go v0.16.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/99designs/gqlgen v0.17.44 h1:OS2wLk/67Y+vXM75XHbwRnNYJcbuJd4OBL76RX3NQQA= +github.com/99designs/gqlgen v0.17.44/go.mod h1:UTCu3xpK2mLI5qcMNw+HKDiEL77it/1XtAjisC4sLwM= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= +github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= +github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= @@ -8,6 +16,9 @@ github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E= +github.com/bool64/shared v0.1.5/go.mod h1:081yz68YC9jeFB3+Bbmno2RFWvGKv1lPKkMP6MHJlPs= +github.com/bradfitz/gomemcache v0.0.0-20170208213004-1952afaa557d/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= @@ -17,106 +28,204 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= -github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= -github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fsnotify/fsnotify v1.4.3-0.20170329110642-4da3e2cfbabc/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/garyburd/redigo v1.1.1-0.20170914051019-70e1b1943d4f/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= -github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f h1:16RtHeWGkJMc80Etb8RPCcKevXGldr57+LOyZt8zOlg= +github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f/go.mod h1:ijRvpgDJDI262hYq/IQVYgf8hd8IHUs93Ol0kvMBAx4= +github.com/golang/lint v0.0.0-20170918230701-e5d664eb928e/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.1.1-0.20171103154506-982329095285/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= -github.com/hashicorp/golang-lru/v2 v2.0.3 h1:kmRrRLlInXvng0SmLxmQpQkpbYAvcXm7NPDrgxJa9mE= -github.com/hashicorp/golang-lru/v2 v2.0.3/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/gregjones/httpcache v0.0.0-20170920190843-316c5e0ff04e/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/hcl v0.0.0-20170914154624-68e816d1c783/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= +github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= +github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE= +github.com/inconshreveable/log15 v0.0.0-20170622235902-74a0988b5f80/go.mod h1:cOaXtrgN4ScfRrD9Bre7U1thNq5RtJ8ZoP4iXVGRj6o= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= +github.com/magiconair/properties v1.7.4-0.20170902060319-8d7837e64d3c/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= -github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= -github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/mattn/go-colorable v0.0.10-0.20170816031813-ad5389df28cd/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.2/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= +github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mitchellh/mapstructure v0.0.0-20170523030023-d0303fe80992/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 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/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= +github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= +github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= +github.com/pelletier/go-toml v1.0.1-0.20170904195809-1d6b12b7cb29/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= -github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= -github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM= -github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= -github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= -github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= +github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= +github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.47.0 h1:p5Cz0FNHo7SnWOmWmoRozVcjEp0bIVU8cV7OShpjL1k= +github.com/prometheus/common v0.47.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= -github.com/ravilushqa/otelgqlgen v0.13.1 h1:V+zFE75iDd2/CSzy5kKnb+Fi09SsE5535wv9U2nUEFE= -github.com/ravilushqa/otelgqlgen v0.13.1/go.mod h1:ZIyWykK2paCuNi9k8gk5edcNSwDJuxZaW90vZXpafxw= +github.com/ravilushqa/otelgqlgen v0.15.0 h1:U85nrlweMXTGaMChUViYM39/MXBZVeVVlpuHq+6eECQ= +github.com/ravilushqa/otelgqlgen v0.15.0/go.mod h1:o+1Eju0VySmgq2BP8Vupz2YrN21Bj7D7imBqu3m2uB8= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/sosodev/duration v1.2.0 h1:pqK/FLSjsAADWY74SyWDCjOcd5l7H8GSnnOGEB9A1Us= +github.com/sosodev/duration v1.2.0/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= +github.com/spf13/afero v0.0.0-20170901052352-ee1bd8ee15a1/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.1.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= +github.com/spf13/jwalterweatherman v0.0.0-20170901151539-12bd96e66386/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.1-0.20170901120850-7aff26db30c1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.0.0/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/vektah/gqlparser/v2 v2.5.6 h1:Ou14T0N1s191eRMZ1gARVqohcbe1e8FrcONScsq8cRU= -github.com/vektah/gqlparser/v2 v2.5.6/go.mod h1:z8xXUff237NntSuH8mLFijZ+1tjV1swDbpDqjJmk6ME= -go.opentelemetry.io/contrib v1.16.1 h1:EpASvVyGx6/ZTlmXzxYfTMZxHROelCeXXa2uLiwltcs= -go.opentelemetry.io/contrib v1.16.1/go.mod h1:gIzjwWFoGazJmtCaDgViqOSJPde2mCWzv60o0bWPcZs= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.44.0 h1:KfYpVmrjI7JuToy5k8XV3nkapjWx48k4E4JOtVstzQI= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.44.0/go.mod h1:SeQhzAEccGVZVEy7aH87Nh0km+utSpo1pTv6eMMop48= -go.opentelemetry.io/contrib/instrumentation/runtime v0.42.0 h1:EbmAUG9hEAMXyfWEasIt2kmh/WmXUznUksChApTgBGc= -go.opentelemetry.io/contrib/instrumentation/runtime v0.42.0/go.mod h1:rD9feqRYP24P14t5kmhNMqsqm1jvKmpx2H2rKVw52V8= -go.opentelemetry.io/otel v1.18.0 h1:TgVozPGZ01nHyDZxK5WGPFB9QexeTMXEH7+tIClWfzs= -go.opentelemetry.io/otel v1.18.0/go.mod h1:9lWqYO0Db579XzVuCKFNPDl4s73Voa+zEck3wHaAYQI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.18.0 h1:IAtl+7gua134xcV3NieDhJHjjOVeJhXAnYf/0hswjUY= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.18.0/go.mod h1:w+pXobnBzh95MNIkeIuAKcHe/Uu/CX2PKIvBP6ipKRA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.18.0 h1:6pu8ttx76BxHf+xz/H77AUZkPF3cwWzXqAUsXhVKI18= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.18.0/go.mod h1:IOmXxPrxoxFMXdNy7lfDmE8MzE61YPcurbUm0SMjerI= -go.opentelemetry.io/otel/exporters/prometheus v0.41.0 h1:A3/bhjP5SmELy8dcpK+uttHeh9Qrh+YnS16/VzrztRQ= -go.opentelemetry.io/otel/exporters/prometheus v0.41.0/go.mod h1:mKuXEMi9suyyNJQ99SZCO0mpWGFe0MIALtjd3r6uo7Q= -go.opentelemetry.io/otel/metric v1.18.0 h1:JwVzw94UYmbx3ej++CwLUQZxEODDj/pOuTCvzhtRrSQ= -go.opentelemetry.io/otel/metric v1.18.0/go.mod h1:nNSpsVDjWGfb7chbRLUNW+PBNdcSTHD4Uu5pfFMOI0k= -go.opentelemetry.io/otel/sdk v1.18.0 h1:e3bAB0wB3MljH38sHzpV/qWrOTCFrdZF2ct9F8rBkcY= -go.opentelemetry.io/otel/sdk v1.18.0/go.mod h1:1RCygWV7plY2KmdskZEDDBs4tJeHG92MdHZIluiYs/M= -go.opentelemetry.io/otel/sdk/metric v0.41.0 h1:c3sAt9/pQ5fSIUfl0gPtClV3HhE18DCVzByD33R/zsk= -go.opentelemetry.io/otel/sdk/metric v0.41.0/go.mod h1:PmOmSt+iOklKtIg5O4Vz9H/ttcRFSNTgii+E1KGyn1w= -go.opentelemetry.io/otel/trace v1.18.0 h1:NY+czwbHbmndxojTEKiSMHkG2ClNH2PwmcHrdo0JY10= -go.opentelemetry.io/otel/trace v1.18.0/go.mod h1:T2+SGJGuYZY3bjj5rgh/hN7KIrlpWC5nS8Mjvzckz+0= -go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= -go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7o2xQ= +github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU= +github.com/vektah/gqlparser/v2 v2.5.11 h1:JJxLtXIoN7+3x6MBdtIP59TP1RANnY7pXOaDnADQSf8= +github.com/vektah/gqlparser/v2 v2.5.11/go.mod h1:1rCcfwB2ekJofmluGWXMSEnPMZgbxzwj6FaZ/4OT8Cc= +github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= +github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= +go.nhat.io/otelsql v0.12.0 h1:/rBhWZiwHFLpCm5SGdafm+Owm0OmGmnF31XWxgecFtY= +go.nhat.io/otelsql v0.12.0/go.mod h1:39Hc9/JDfCl7NGrBi1uPP3QPofqwnC/i5SFd7gtDMWM= +go.opentelemetry.io/contrib v1.23.0 h1:5f6bvGoHE/7lcolc1jCA4Vzq2tnPs4tfqL1M/yfjbOA= +go.opentelemetry.io/contrib v1.23.0/go.mod h1:usW9bPlrjHiJFbK0a6yK/M5wNHs3nLmtrT3vzhoD3co= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0 h1:doUP+ExOpH3spVTLS0FcWGLnQrPct/hD/bCPbDRUEAU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0/go.mod h1:rdENBZMT2OE6Ne/KLwpiXudnAsbdrdBaqBvTN8M8BgA= +go.opentelemetry.io/contrib/instrumentation/runtime v0.48.0 h1:dJlCKeq+zmO5Og4kgxqPvvJrzuD/mygs1g/NYM9dAsU= +go.opentelemetry.io/contrib/instrumentation/runtime v0.48.0/go.mod h1:p+hpBCpLHpuUrR0lHgnHbUnbCBll1IhrcMIlycC+xYs= +go.opentelemetry.io/otel v1.23.1 h1:Za4UzOqJYS+MUczKI320AtqZHZb7EqxO00jAHE0jmQY= +go.opentelemetry.io/otel v1.23.1/go.mod h1:Td0134eafDLcTS4y+zQ26GE8u3dEuRBiBCTUIRHaikA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.23.1 h1:o8iWeVFa1BcLtVEV0LzrCxV2/55tB3xLxADr6Kyoey4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.23.1/go.mod h1:SEVfdK4IoBnbT2FXNM/k8yC08MrfbhWk3U4ljM8B3HE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.23.1 h1:cfuy3bXmLJS7M1RZmAL6SuhGtKUp2KEsrm00OlAXkq4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.23.1/go.mod h1:22jr92C6KwlwItJmQzfixzQM3oyyuYLCfHiMY+rpsPU= +go.opentelemetry.io/otel/exporters/prometheus v0.45.2 h1:pe2Jqk1K18As0RCw7J08QhgXNqr+6npx0a5W4IgAFA8= +go.opentelemetry.io/otel/exporters/prometheus v0.45.2/go.mod h1:B38pscHKI6bhFS44FDw0eFU3iqG3ASNIvY+fZgR5sAc= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v0.40.0 h1:hf7JSONqAuXT1PDYYlVhKNMPLe4060d+4RFREcv7X2c= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v0.40.0/go.mod h1:IxD5qbw/XcnFB7i5k4d7J1aW5iBU2h4DgSxtk4YqR4c= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.17.0 h1:Ut6hgtYcASHwCzRHkXEtSsM251cXJPW+Z9DyLwEn6iI= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.17.0/go.mod h1:TYeE+8d5CjrgBa0ZuRaDeMpIC1xZ7atg4g+nInjuSjc= +go.opentelemetry.io/otel/metric v1.23.1 h1:PQJmqJ9u2QaJLBOELl1cxIdPcpbwzbkjfEyelTl2rlo= +go.opentelemetry.io/otel/metric v1.23.1/go.mod h1:mpG2QPlAfnK8yNhNJAxDZruU9Y1/HubbC+KyH8FaCWI= +go.opentelemetry.io/otel/sdk v1.23.1 h1:O7JmZw0h76if63LQdsBMKQDWNb5oEcOThG9IrxscV+E= +go.opentelemetry.io/otel/sdk v1.23.1/go.mod h1:LzdEVR5am1uKOOwfBWFef2DCi1nu3SA8XQxx2IerWFk= +go.opentelemetry.io/otel/sdk/metric v1.23.1 h1:T9/8WsYg+ZqIpMWwdISVVrlGb/N0Jr1OHjR/alpKwzg= +go.opentelemetry.io/otel/sdk/metric v1.23.1/go.mod h1:8WX6WnNtHCgUruJ4TJ+UssQjMtpxkpX0zveQC8JG/E0= +go.opentelemetry.io/otel/trace v1.23.1 h1:4LrmmEd8AU2rFvU1zegmvqW7+kWarxtNOPyeL6HmYY8= +go.opentelemetry.io/otel/trace v1.23.1/go.mod h1:4IpnpJFwr1mo/6HL8XIPJaE9y0+u1KcVmuW7dwFSVrI= +go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI= +go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY= +go.sour.is/passwd v0.2.0 h1:eFLrvcayaS2e8ItjTU/tzBWtt1Am9xH97uBGqCCxdkk= +go.sour.is/passwd v0.2.0/go.mod h1:xDqWTLiztFhr1KvUh//lvmJfMg+9piWt7K+d1JX3n0s= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE= +golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= +golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/oauth2 v0.0.0-20170912212905-13449ad91cb2/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20170517211232-f52d1811a629/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.0.0-20170424234030-8be79e1e0910/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= +golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 h1:Z0hjGZePRE0ZBWotvtrwxFNrNE9CUAGtplaDK5NNI/g= -google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 h1:FmF5cCW94Ij59cfpoLiwTgodWmm60eEV0CjlsVg2fuw= -google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= -google.golang.org/grpc v1.58.0 h1:32JY8YpPMSR45K+c3o6b8VL73V+rR8k+DeMIr4vRH8o= -google.golang.org/grpc v1.58.0/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= +google.golang.org/api v0.0.0-20170921000349-586095a6e407/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20170918111702-1e559d0a00ee/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y= +google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s= +google.golang.org/genproto/googleapis/api v0.0.0-20240213162025-012b6fc9bca9 h1:4++qSzdWBUy9/2x8L5KZgwZw+mjJZ2yDSCGMVM0YzRs= +google.golang.org/genproto/googleapis/api v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:PVreiBMirk8ypES6aw9d4p6iiBNSIfZEBqr3UGoAi2E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240213162025-012b6fc9bca9 h1:hZB7eLIaYlW9qXRfCq/qDaPdbeY3757uARz5Vvfv+cY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:YUWgXUFRPfoYK1IHMuxH5K6nPEXSCzIMljnQ59lLRCk= +google.golang.org/grpc v1.2.1-0.20170921194603-d4b75ebd4f9f/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.61.1 h1:kLAiWrZs7YeDM6MumDe7m3y4aM6wacLzM1Y/wiLP9XY= +google.golang.org/grpc v1.61.1/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= +modernc.org/libc v1.41.0 h1:g9YAc6BkKlgORsUWj+JwqoB1wU3o4DE3bM3yvA3k+Gk= +modernc.org/libc v1.41.0/go.mod h1:w0eszPsiXoOnoMJgrXjglgLuDy/bt5RR4y3QzUUeodY= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E= +modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= +modernc.org/sqlite v1.29.1 h1:19GY2qvWB4VPw0HppFlZCPAbmxFU41r+qjKZQdQ1ryA= +modernc.org/sqlite v1.29.1/go.mod h1:hG41jCYxOAOoO6BRK66AdRlmOcDzXf7qnwlwjUIOqa0= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/ident/ident.go b/ident/ident.go new file mode 100644 index 0000000..7c85a56 --- /dev/null +++ b/ident/ident.go @@ -0,0 +1,115 @@ +package ident + +import ( + "context" + "fmt" + "io" + "net/http" + "sort" + + "github.com/oklog/ulid/v2" + "go.sour.is/passwd" +) + +// Ident interface for a logged in user +type Ident interface { + Identity() string + Session() *SessionInfo +} + +type SessionInfo struct { + SessionID ulid.ULID + Active bool +} + +func (s *SessionInfo) Session() *SessionInfo { return s } + +// Handler handler function to read ident from HTTP request +type Handler interface { + ReadIdent(r *http.Request) (Ident, error) +} +type HandleGet interface { + GetIdent(context.Context /* identity */, string) (Ident, error) +} +type HandleRegister interface { + RegisterIdent(ctx context.Context, identity, displayName string, passwd []byte) (Ident, error) +} + +type source struct { + Handler + priority int +} + +var contextKey = struct{ key string }{"ident"} + +func FromContext(ctx context.Context) Ident { + if id, ok := ctx.Value(contextKey).(Ident); ok { + return id + } + return Anonymous +} + +type IDM struct { + rand io.Reader + sources []source + pwd *passwd.Passwd +} + +func NewIDM(pwd *passwd.Passwd, rand io.Reader) *IDM { + return &IDM{pwd: pwd, rand:rand} +} + +func (idm *IDM) Add(p int, h Handler) { + idm.sources = append(idm.sources, source{priority: p, Handler: h}) + sort.Slice(idm.sources, func(i, j int) bool { return idm.sources[i].priority < idm.sources[j].priority }) +} + +func (idm *IDM) Passwd(pass, hash []byte) ([]byte, error) { + return idm.pwd.Passwd(pass, hash) +} + +// ReadIdent read ident from a list of ident handlers +func (idm *IDM) ReadIdent(r *http.Request) (Ident, error) { + for _, source := range idm.sources { + u, err := source.ReadIdent(r) + if err != nil { + return Anonymous, err + } + + if u.Session().Active { + return u, err + } + } + + return Anonymous, nil +} + +func (idm *IDM) RegisterIdent(ctx context.Context, identity, displayName string, passwd []byte) (Ident, error) { + for _, source := range idm.sources { + if source, ok := source.Handler.(HandleRegister); ok { + return source.RegisterIdent(ctx, identity, displayName, passwd) + } + } + + return nil, fmt.Errorf("no HandleRegister source registered") +} + +func (idm *IDM) GetIdent(ctx context.Context, identity string) (Ident, error) { + for _, source := range idm.sources { + if source, ok := source.Handler.(HandleGet); ok { + return source.GetIdent(ctx, identity) + } + } + + return nil, fmt.Errorf("no HandleGet source registered") +} + +func (idm *IDM) NewSessionInfo() (session SessionInfo, err error) { + session.SessionID, err = ulid.New(ulid.Now(), idm.rand) + if err != nil { + return + } + session.Active = true + + return session, nil +} diff --git a/ident/null-user.go b/ident/null-user.go new file mode 100644 index 0000000..f67a121 --- /dev/null +++ b/ident/null-user.go @@ -0,0 +1,75 @@ +package ident + +import "net/http" + +// nullUser implements a null ident +type nullUser struct { + identity string + aspect string + displayName string + SessionInfo +} + +// Anonymous is a logged out user +var Anonymous = NewNullUser("anon", "none", "Guest User", false) + +// NewNullUser creates a null user ident +func NewNullUser(ident, aspect, name string, active bool) *nullUser { + return &nullUser{ident, aspect, name, SessionInfo{Active: active}} +} + +func (id nullUser) String() string { + return "id: " + id.identity + " dn: " + id.displayName +} + +// GetIdentity returns identity +func (m nullUser) Identity() string { + return m.identity +} + +// GetAspect returns aspect +func (m nullUser) Aspect() string { + return m.aspect +} + +// HasRole returns true if matches role +func (m nullUser) Role(r ...string) bool { + return m.Active +} + +// HasGroup returns true if matches group +func (m nullUser) Group(g ...string) bool { + return m.Active +} + +// GetGroups returns empty list +func (m nullUser) Groups() []string { + return []string{} +} + +// GetRoles returns empty list +func (m nullUser) Roles() []string { + return []string{} +} + +// GetMeta returns empty list +func (m nullUser) Meta() map[string]string { + return make(map[string]string) +} + +// IsActive returns true if active +func (m nullUser) IsActive() bool { + return m.Active +} + +// GetDisplay returns display name +func (m nullUser) Display() string { + return m.displayName +} + +// MakeHandlerFunc returns handler func +func (m nullUser) HandlerFunc() func(r *http.Request) Ident { + return func(r *http.Request) Ident { + return &m + } +} diff --git a/ident/routes.go b/ident/routes.go new file mode 100644 index 0000000..cb3702b --- /dev/null +++ b/ident/routes.go @@ -0,0 +1,360 @@ +package ident + +import ( + "context" + "errors" + "fmt" + "net/http" + "time" + + "github.com/oklog/ulid/v2" + + "go.sour.is/passwd" + "go.sour.is/pkg/lg" + "go.sour.is/pkg/locker" +) + +var ( + loginForm = func(nick string, valid bool) string { + indicator := "" + if !valid { + indicator = `class="invalid"` + } + if nick != "" { + nick = `value="` + nick + `"` + } + return ` +
` + } + logoutForm = func(display string) string { + return `` + } + registerForm = ` + ` +) + +type sessions map[ulid.ULID]Ident + +type root struct { + idm *IDM + sessions *locker.Locked[sessions] +} + +func NewHTTP(idm *IDM) *root { + sessions := make(sessions) + return &root{ + idm: idm, + sessions: locker.New(sessions), + } +} + +func (s *root) RegisterHTTP(mux *http.ServeMux) { + mux.HandleFunc("/ident", s.get) + mux.HandleFunc("/ident/register", s.register) + mux.HandleFunc("/ident/login", s.login) + mux.HandleFunc("/ident/logout", s.logout) +} +func (s *root) RegisterAPIv1(mux *http.ServeMux) { + mux.HandleFunc("POST /ident", s.registerV1) + mux.HandleFunc("POST /ident/session", s.loginV1) + mux.HandleFunc("DELETE /ident/session", s.logoutV1) + mux.HandleFunc("GET /ident", s.getV1) +} +func (s *root) RegisterMiddleware(hdlr http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx, span := lg.Span(r.Context()) + defer span.End() + + cookie, err := r.Cookie("sour.is-ident") + span.RecordError(err) + if err != nil { + hdlr.ServeHTTP(w, r) + return + } + + sessionID, err := ulid.Parse(cookie.Value) + span.RecordError(err) + + var id Ident = Anonymous + if err == nil { + err = s.sessions.Use(ctx, func(ctx context.Context, sessions sessions) error { + if session, ok := sessions[sessionID]; ok { + id = session + } + return nil + }) + } + span.RecordError(err) + + r = r.WithContext(context.WithValue(r.Context(), contextKey, id)) + + hdlr.ServeHTTP(w, r) + }) +} +func (s *root) createSession(ctx context.Context, id Ident) error { + return s.sessions.Use(ctx, func(ctx context.Context, sessions sessions) error { + sessions[id.Session().SessionID] = id + return nil + }) +} +func (s *root) destroySession(ctx context.Context, id Ident) error { + session := id.Session() + session.Active = false + + return s.sessions.Use(ctx, func(ctx context.Context, sessions sessions) error { + delete(sessions, session.SessionID) + return nil + }) +} + +func (s *root) getV1(w http.ResponseWriter, r *http.Request) { + ctx, span := lg.Span(r.Context()) + defer span.End() + + var id Ident = FromContext(ctx) + if id == nil { + http.Error(w, "NO_AUTH", http.StatusUnauthorized) + return + } + fmt.Fprint(w, id) +} +func (s *root) loginV1(w http.ResponseWriter, r *http.Request) { + ctx, span := lg.Span(r.Context()) + defer span.End() + + id, err := s.idm.ReadIdent(r) + span.RecordError(err) + if err != nil { + http.Error(w, "ERR", http.StatusInternalServerError) + return + } + if !id.Session().Active { + http.Error(w, "NO_AUTH", http.StatusUnauthorized) + return + } + + err = s.createSession(ctx, id) + if err != nil { + span.RecordError(err) + http.Error(w, "ERR", http.StatusInternalServerError) + return + } + + http.SetCookie(w, &http.Cookie{ + Name: "sour.is-ident", + Value: id.Session().SessionID.String(), + Expires: time.Time{}, + Path: "/", + Secure: false, + HttpOnly: true, + }) + + fmt.Fprint(w, id) +} +func (s *root) logoutV1(w http.ResponseWriter, r *http.Request) { + ctx, span := lg.Span(r.Context()) + defer span.End() + + if r.Method != http.MethodPost { + http.Error(w, "ERR", http.StatusMethodNotAllowed) + return + } + + err := s.destroySession(ctx, FromContext(ctx)) + if err != nil { + span.RecordError(err) + http.Error(w, "NO_AUTH", http.StatusUnauthorized) + return + } + + http.SetCookie(w, &http.Cookie{Name: "sour.is-ident", MaxAge: -1}) + + http.Error(w, "GONE", http.StatusGone) +} +func (s *root) registerV1(w http.ResponseWriter, r *http.Request) { + ctx, span := lg.Span(r.Context()) + defer span.End() + + if r.Method != http.MethodPost { + http.Error(w, "ERR", http.StatusMethodNotAllowed) + return + } + r.ParseForm() + + identity := r.Form.Get("identity") + display := r.Form.Get("displayName") + passwd, err := s.idm.Passwd([]byte(r.Form.Get("passwd")), nil) + if err != nil { + http.Error(w, "ERR", http.StatusInternalServerError) + return + } + + _, err = s.idm.RegisterIdent(ctx, identity, display, passwd) + if err != nil { + http.Error(w, "ERR", http.StatusInternalServerError) + return + } + + http.Error(w, "OK "+identity, http.StatusCreated) +} + +func (s *root) get(w http.ResponseWriter, r *http.Request) { + ctx, span := lg.Span(r.Context()) + defer span.End() + + var id Ident = FromContext(ctx) + if id == nil { + http.Error(w, loginForm("", true), http.StatusOK) + return + } + + if !id.Session().Active { + http.Error(w, loginForm("", true), http.StatusOK) + return + } + + display := id.Identity() + if id, ok := id.(interface{ DisplayName() string }); ok { + display = id.DisplayName() + } + fmt.Fprint(w, logoutForm(display)) +} +func (s *root) login(w http.ResponseWriter, r *http.Request) { + ctx, span := lg.Span(r.Context()) + defer span.End() + + if r.Method == http.MethodGet { + fmt.Fprint(w, loginForm("", true)) + return + } + + id, err := s.idm.ReadIdent(r) + span.RecordError(err) + if err != nil { + if errors.Is(err, passwd.ErrNoMatch) { + http.Error(w, loginForm("", false), http.StatusOK) + return + } + + http.Error(w, "ERROR", http.StatusInternalServerError) + return + } + + if !id.Session().Active { + http.Error(w, loginForm("", false), http.StatusOK) + return + } + + err = s.createSession(ctx, id) + span.RecordError(err) + if err != nil { + http.Error(w, "ERROR", http.StatusInternalServerError) + return + } + + http.SetCookie(w, &http.Cookie{ + Name: "sour.is-ident", + Value: id.Session().SessionID.String(), + Expires: time.Time{}, + Path: "/", + Secure: false, + HttpOnly: true, + }) + + display := id.Identity() + if id, ok := id.(interface{ DisplayName() string }); ok { + display = id.DisplayName() + } + fmt.Fprint(w, logoutForm(display)) +} +func (s *root) logout(w http.ResponseWriter, r *http.Request) { + ctx, span := lg.Span(r.Context()) + defer span.End() + + if r.Method != http.MethodPost { + http.Error(w, "ERR", http.StatusMethodNotAllowed) + return + } + + http.SetCookie(w, &http.Cookie{Name: "sour.is-ident", MaxAge: -1}) + + err := s.destroySession(ctx, FromContext(ctx)) + span.RecordError(err) + if err != nil { + http.Error(w, loginForm("", true), http.StatusUnauthorized) + return + } + + fmt.Fprint(w, loginForm("", true)) +} +func (s *root) register(w http.ResponseWriter, r *http.Request) { + ctx, span := lg.Span(r.Context()) + defer span.End() + + if r.Method == http.MethodGet { + fmt.Fprint(w, registerForm) + return + } + + if r.Method != http.MethodPost { + http.Error(w, "ERR", http.StatusMethodNotAllowed) + return + } + + r.ParseForm() + identity := r.Form.Get("identity") + display := r.Form.Get("displayName") + + passwd, err := s.idm.Passwd([]byte(r.Form.Get("passwd")), nil) + if err != nil { + http.Error(w, "ERR", http.StatusInternalServerError) + return + } + + id, err := s.idm.RegisterIdent(ctx, identity, display, passwd) + if err != nil { + http.Error(w, "ERR", http.StatusInternalServerError) + return + } + + if !id.Session().Active { + http.Error(w, loginForm("", false), http.StatusUnauthorized) + return + } + + err = s.createSession(ctx, id) + span.RecordError(err) + if err != nil { + http.Error(w, "ERROR", http.StatusInternalServerError) + return + } + + http.SetCookie(w, &http.Cookie{ + Name: "sour.is-ident", + Value: id.Session().SessionID.String(), + Expires: time.Time{}, + Path: "/", + Secure: false, + HttpOnly: true, + }) + + display = id.Identity() + if id, ok := id.(interface{ DisplayName() string }); ok { + display = id.DisplayName() + } + + http.Error(w, logoutForm(display), http.StatusCreated) +} diff --git a/ident/source/mercury.go b/ident/source/mercury.go new file mode 100644 index 0000000..c54ed01 --- /dev/null +++ b/ident/source/mercury.go @@ -0,0 +1,169 @@ +package source + +import ( + "context" + "fmt" + "net/http" + "strings" + "time" + + "go.sour.is/pkg/lg" + "go.sour.is/pkg/mercury" + "go.sour.is/pkg/ident" +) + +type registry interface { + GetIndex(ctx context.Context, match, search string) (c mercury.Config, err error) + GetConfig(ctx context.Context, match, search, fields string) (mercury.Config, error) + WriteConfig(ctx context.Context, spaces mercury.Config) error +} + +type mercuryIdent struct { + identity string + display string + passwd []byte + ident.SessionInfo +} + +func (id *mercuryIdent) Identity() string { return id.identity } +func (id *mercuryIdent) DisplayName() string { return id.display } +func (id *mercuryIdent) Space() string { return "mercury.@" + id.identity } + +func (id *mercuryIdent) FromConfig(cfg mercury.Config) error { + if id == nil { + return fmt.Errorf("nil ident") + } + + for _, s := range cfg { + if !strings.HasPrefix(s.Space, "mercury.") { + continue + } + if id.identity == "" { + _, id.identity, _ = strings.Cut(s.Space, ".@") + id.identity, _, _ = strings.Cut(id.identity, ".") + } + + switch { + case strings.HasSuffix(s.Space, ".ident"): + id.passwd = []byte(s.FirstValue("passwd").First()) + default: + id.display = s.FirstValue("displayName").First() + } + } + return nil +} + +func (id *mercuryIdent) ToConfig() mercury.Config { + space := id.Space() + return mercury.Config{ + &mercury.Space{ + Space: space, + List: []mercury.Value{ + { + Space: space, + Seq: 1, + Name: "displayName", + Values: []string{id.display}, + }, + { + Space: space, + Seq: 2, + Name: "lastLogin", + Values: []string{time.UnixMilli(int64(id.Session().SessionID.Time())).Format(time.RFC3339)}, + }, + }, + }, + &mercury.Space{ + Space: space + ".ident", + List: []mercury.Value{ + { + Space: space + ".ident", + Seq: 1, + Name: "passwd", + Values: []string{string(id.passwd)}, + }, + }, + }, + } +} + +func (id *mercuryIdent) String() string { + return "id: " + id.identity + " sp: " + id.Space() + " dn: " + id.display // + " ps: " + string(id.passwd) +} + +func (id *mercuryIdent) HasRole(r ...string) bool { + return false +} + +type mercurySource struct { + r registry + idm *ident.IDM +} + +func NewMercury(r registry, pwd *ident.IDM) *mercurySource { + return &mercurySource{r, pwd} +} + +func (s *mercurySource) ReadIdent(r *http.Request) (ident.Ident, error) { + ctx, span := lg.Span(r.Context()) + defer span.End() + + if r.Method != http.MethodPost { + return nil, fmt.Errorf("method not allowed") + } + r.ParseForm() + id := &mercuryIdent{ + identity: r.Form.Get("identity"), + passwd: []byte(r.Form.Get("passwd")), + } + + space := id.Space() + c, err := s.r.GetConfig(ctx, "trace:"+space+".ident", "", "") + if err != nil { + span.RecordError(err) + return id, err + } + var current mercuryIdent + current.FromConfig(c) + if len(current.passwd) == 0 { + return nil, fmt.Errorf("not registered") + } + _, err = s.idm.Passwd(id.passwd, current.passwd) + if err != nil { + return id, err + } + current.SessionInfo, err = s.idm.NewSessionInfo() + if err != nil { + return id, err + } + + err = s.r.WriteConfig(ctx, current.ToConfig()) + if err != nil { + return ¤t, err + } + + return ¤t, nil +} +func (s *mercurySource) RegisterIdent(ctx context.Context, identity, display string, passwd []byte) (ident.Ident, error) { + ctx, span := lg.Span(ctx) + defer span.End() + + id := &mercuryIdent{identity: identity, display: display, passwd: passwd} + space := id.Space() + + _, err := s.r.GetIndex(ctx, space, "") + if err != nil { + return nil, err + } + + id.SessionInfo, err = s.idm.NewSessionInfo() + if err != nil { + return id, err + } + + err = s.r.WriteConfig(ctx, id.ToConfig()) + if err != nil { + return nil, err + } + return id, nil +} diff --git a/lg/tracer.go b/lg/tracer.go index 99b0190..5e98b69 100644 --- a/lg/tracer.go +++ b/lg/tracer.go @@ -58,17 +58,56 @@ type wrapSpan struct { trace.Span } +func LogQuery(q string, args []any, err error) (string, trace.EventOption) { + var attrs []attribute.KeyValue + for k, v := range args { + var attr attribute.KeyValue + switch v:=v.(type) { + case int64: + attr = attribute.Int64( + fmt.Sprintf("$%d", k), + v, + ) + case string: + attr = attribute.String( + fmt.Sprintf("$%d", k), + v, + ) + default: + attr = attribute.String( + fmt.Sprintf("$%d", k), + fmt.Sprint(v), + ) + } + + attrs = append(attrs, attr) + } + + return q, trace.WithAttributes(attrs...) +} + func (w wrapSpan) AddEvent(name string, options ...trace.EventOption) { w.Span.AddEvent(name, options...) cfg := trace.NewEventConfig(options...) attrs := cfg.Attributes() - args := make([]any, len(attrs)*2) + args := make([]any, len(attrs)) for i, a := range attrs { - args[2*i] = a.Key - args[2*i+1] = a.Value + switch a.Value.Type() { + case attribute.BOOL: + args[i] = slog.Bool(string(a.Key), a.Value.AsBool()) + case attribute.INT64: + args[i] = slog.Int64(string(a.Key), a.Value.AsInt64()) + case attribute.FLOAT64: + args[i] = slog.Float64(string(a.Key), a.Value.AsFloat64()) + case attribute.STRING: + args[i] = slog.String(string(a.Key), a.Value.AsString()) + default: + args[i] = slog.Any(string(a.Key), a.Value.AsInterface()) + } + } slog.Debug(name, args...) diff --git a/mercury/app/app_test.go b/mercury/app/app_test.go new file mode 100644 index 0000000..3824501 --- /dev/null +++ b/mercury/app/app_test.go @@ -0,0 +1,157 @@ +package app + +import ( + "context" + "reflect" + "testing" + + "go.sour.is/pkg/mercury" + "go.sour.is/pkg/ident" +) + +type mockUser struct { + roles map[string]struct{} + ident.SessionInfo +} + +func (m *mockUser) Identity() string { return "user" } +func (m *mockUser) HasRole(roles ...string) bool { + var found bool + for _, role := range roles { + if _, ok := m.roles[role]; ok { + found = true + } + } + + return found +} + +func Test_appConfig_GetRules(t *testing.T) { + type args struct { + u ident.Ident + } + tests := []struct { + name string + args args + wantLis mercury.Rules + }{ + {"normal", args{&mockUser{}}, nil}, + { + "admin", + args{ + &mockUser{ + SessionInfo: ident.SessionInfo{Active: true}, + roles: map[string]struct{}{"admin": {}}, + }, + }, + mercury.Rules{ + mercury.Rule{ + Role: "read", + Type: "NS", + Match: "mercury.source.*", + }, + mercury.Rule{ + Role: "read", + Type: "NS", + Match: "mercury.priority", + }, + mercury.Rule{ + Role: "read", + Type: "NS", + Match: "mercury.host", + }, + mercury.Rule{ + Role: "read", + Type: "NS", + Match: "mercury.environ", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := mercuryEnviron{} + if gotLis, _ := a.GetRules(context.TODO(), tt.args.u); !reflect.DeepEqual(gotLis, tt.wantLis) { + t.Errorf("appConfig.GetRules() = %v, want %v", gotLis, tt.wantLis) + } + }) + } +} + +// func Test_appConfig_GetIndex(t *testing.T) { +// type args struct { +// search mercury.NamespaceSearch +// in1 *rsql.Program +// } +// tests := []struct { +// name string +// args args +// wantLis mercury.Config +// }{ +// {"nil", args{ +// nil, +// nil, +// }, nil}, + +// {"app.settings", args{ +// mercury.ParseNamespace("app.settings"), +// nil, +// }, mercury.Config{&mercury.Space{Space: "app.settings"}}}, +// } +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// a := mercuryEnviron{} +// if gotLis, _ := a.GetIndex(tt.args.search, tt.args.in1); !reflect.DeepEqual(gotLis, tt.wantLis) { +// t.Errorf("appConfig.GetIndex() = %#v, want %#v", gotLis, tt.wantLis) +// } +// }) +// } +// } + +// func Test_appConfig_GetObjects(t *testing.T) { +// cfg, err := mercury.ParseText(strings.NewReader(` +// @mercury.source.mercury-settings.default +// match :0 * +// `)) + +// type args struct { +// search mercury.NamespaceSearch +// in1 *rsql.Program +// in2 []string +// } +// tests := []struct { +// name string +// args args +// wantLis mercury.Config +// }{ +// {"nil", args{ +// nil, +// nil, +// nil, +// }, nil}, + +// {"app.settings", args{ +// mercury.ParseNamespace("app.settings"), +// nil, +// nil, +// }, mercury.Config{ +// &mercury.Space{ +// Space: "app.settings", +// List: []mercury.Value{{ +// Space: "app.settings", +// Name: "app.setting", +// Values: []string{"TRUE"}}, +// }, +// }, +// }}, +// } +// for _, tt := range tests { +// cfg, err := +// t.Run(tt.name, func(t *testing.T) { +// a := appConfig{cfg: } +// if gotLis, _ := a.GetConfig(tt.args.search, tt.args.in1, tt.args.in2); !reflect.DeepEqual(gotLis, tt.wantLis) { +// t.Errorf("appConfig.GetIndex() = %#v, want %#v", gotLis, tt.wantLis) +// } +// }) +// } +// } diff --git a/mercury/app/default-rules.go b/mercury/app/default-rules.go new file mode 100644 index 0000000..eb7e841 --- /dev/null +++ b/mercury/app/default-rules.go @@ -0,0 +1,98 @@ +package app + +import ( + "context" + "strings" + + "go.sour.is/pkg/mercury" + "go.sour.is/pkg/ident" +) + +type mercuryDefault struct { + name string + cfg mercury.SpaceMap +} + +var ( + _ mercury.GetRules = (*mercuryDefault)(nil) + + _ mercury.GetIndex = (*mercuryEnviron)(nil) + _ mercury.GetConfig = (*mercuryEnviron)(nil) + _ mercury.GetRules = (*mercuryEnviron)(nil) +) + +// GetRules returns default rules for user role. +func (app *mercuryDefault) GetRules(ctx context.Context, id ident.Ident) (lis mercury.Rules, err error) { + identity := id.Identity() + + lis = append(lis, + mercury.Rule{ + Role: "write", + Type: "NS", + Match: "mercury.@" + identity, + }, + mercury.Rule{ + Role: "write", + Type: "NS", + Match: "mercury.@" + identity + ".*", + }, + ) + + groups := groups(identity, &app.cfg) + + if s, ok := app.cfg.Space("mercury.policy."+app.name); ok { + for _, p := range s.List { + if groups.Has(p.Name) { + for _, r := range p.Values { + fds := strings.Fields(r) + if len(fds) < 3 { + continue + } + lis = append(lis, mercury.Rule{ + Role: fds[0], + Type: fds[1], + Match: fds[2], + }) + } + } + } + } + + if u, ok := id.(hasRole); groups.Has("admin") || ok && u.HasRole("admin") { + lis = append(lis, + mercury.Rule{ + Role: "admin", + Type: "NS", + Match: "*", + }, + mercury.Rule{ + Role: "write", + Type: "NS", + Match: "*", + }, + mercury.Rule{ + Role: "admin", + Type: "GR", + Match: "*", + }, + ) + } else if u.HasRole("write") { + lis = append(lis, + mercury.Rule{ + Role: "write", + Type: "NS", + Match: "*", + }, + ) + } else if u.HasRole("read") { + lis = append(lis, + mercury.Rule{ + Role: "read", + Type: "NS", + Match: "*", + }, + ) + } + + return lis, nil +} diff --git a/mercury/app/environ.go b/mercury/app/environ.go new file mode 100644 index 0000000..9a8aa76 --- /dev/null +++ b/mercury/app/environ.go @@ -0,0 +1,288 @@ +package app + +import ( + "context" + "fmt" + "os" + "os/user" + "sort" + "strings" + + "go.sour.is/pkg/mercury" + "go.sour.is/pkg/ident" + "go.sour.is/pkg/rsql" + "go.sour.is/pkg/set" +) + +const ( + mercurySource = "mercury.source.*" + mercuryPriority = "mercury.priority" + mercuryHost = "mercury.host" + appDotEnviron = "mercury.environ" +) +var ( + mercuryPolicy = func(id string) string { return "mercury.@" + id + ".policy" } +) + +func Register(name string, cfg mercury.SpaceMap) { + for _, c := range cfg { + c.Tags = append(c.Tags, "RO") + } + mercury.Registry.Register("mercury-default", func(s *mercury.Space) any { return &mercuryDefault{name: name, cfg: cfg} }) + mercury.Registry.Register("mercury-environ", func(s *mercury.Space) any { return &mercuryEnviron{cfg: cfg, lookup: mercury.Registry.GetRules} }) +} + +type hasRole interface { + HasRole(r ...string) bool +} + +type mercuryEnviron struct { + cfg mercury.SpaceMap + lookup func(context.Context, ident.Ident) (mercury.Rules, error) +} + +// Index returns nil +func (app *mercuryEnviron) GetIndex(ctx context.Context, search mercury.NamespaceSearch, _ *rsql.Program) (lis mercury.Config, err error) { + + if search.Match(mercurySource) { + for _, s := range app.cfg.ToArray() { + if search.Match(s.Space) { + lis = append(lis, &mercury.Space{Space: s.Space, Tags: []string{"RO"}}) + } + } + } + + if search.Match(mercuryPriority) { + lis = append(lis, &mercury.Space{Space: mercuryPriority, Tags: []string{"RO"}}) + } + + if search.Match(mercuryHost) { + lis = append(lis, &mercury.Space{Space: mercuryHost, Tags: []string{"RO"}}) + } + + if search.Match(appDotEnviron) { + lis = append(lis, &mercury.Space{Space: appDotEnviron, Tags: []string{"RO"}}) + } + if id := ident.FromContext(ctx); id != nil { + identity := id.Identity() + match := mercuryPolicy(identity) + if search.Match(match) { + lis = append(lis, &mercury.Space{Space: match, Tags: []string{"RO"}}) + } + } + return +} + +// Objects returns nil +func (app *mercuryEnviron) GetConfig(ctx context.Context, search mercury.NamespaceSearch, _ *rsql.Program, _ []string) (lis mercury.Config, err error) { + if search.Match(mercurySource) { + for _, s := range app.cfg.ToArray() { + if search.Match(s.Space) { + lis = append(lis, s) + } + } + } + + if search.Match(mercuryPriority) { + space := mercury.Space{ + Space: mercuryPriority, + Tags: []string{"RO"}, + } + + // for i, key := range mercury.Registry { + // space.List = append(space.List, mercury.Value{ + // Space: appDotPriority, + // Seq: uint64(i), + // Name: key.Match, + // Values: []string{fmt.Sprint(key.Priority)}, + // }) + // } + + lis = append(lis, &space) + } + + if search.Match(mercuryHost) { + if usr, err := user.Current(); err == nil { + space := mercury.Space{ + Space: mercuryHost, + Tags: []string{"RO"}, + } + + hostname, _ := os.Hostname() + wd, _ := os.Getwd() + grp, _ := usr.GroupIds() + space.List = []mercury.Value{ + { + Space: mercuryHost, + Seq: 1, + Name: "hostname", + Values: []string{hostname}, + }, + { + Space: mercuryHost, + Seq: 2, + Name: "username", + Values: []string{usr.Username}, + }, + { + Space: mercuryHost, + Seq: 3, + Name: "uid", + Values: []string{usr.Uid}, + }, + { + Space: mercuryHost, + Seq: 4, + Name: "gid", + Values: []string{usr.Gid}, + }, + { + Space: mercuryHost, + Seq: 5, + Name: "display", + Values: []string{usr.Name}, + }, + { + Space: mercuryHost, + Seq: 6, + Name: "home", + Values: []string{usr.HomeDir}, + }, + { + Space: mercuryHost, + Seq: 7, + Name: "groups", + Values: grp, + }, + { + Space: mercuryHost, + Seq: 8, + Name: "pid", + Values: []string{fmt.Sprintf("%v", os.Getpid())}, + }, + { + Space: mercuryHost, + Seq: 9, + Name: "wd", + Values: []string{wd}, + }, + } + + lis = append(lis, &space) + } + } + + if search.Match(appDotEnviron) { + env := os.Environ() + space := mercury.Space{ + Space: appDotEnviron, + Tags: []string{"RO"}, + } + + sort.Strings(env) + for i, s := range env { + key, val, _ := strings.Cut(s, "=") + + vals := []string{val} + if strings.Contains(key, "PATH") || strings.Contains(key, "XDG") { + vals = strings.Split(val, ":") + } + + space.List = append(space.List, mercury.Value{ + Space: appDotEnviron, + Seq: uint64(i), + Name: key, + Values: vals, + }) + } + lis = append(lis, &space) + } + + if id := ident.FromContext(ctx); id != nil { + identity := id.Identity() + groups := groups(identity, &app.cfg) + match := mercuryPolicy(identity) + if search.Match(match) { + space := &mercury.Space{ + Space: match, + Tags: []string{"RO"}, + } + + lis = append(lis, space) + rules, err := app.lookup(ctx, id) + if err != nil { + space.AddNotes(err.Error()) + } else { + k := mercury.NewValue("groups") + k.AddValues(strings.Join(groups.Values(), " ")) + space.AddKeys(k) + + k = mercury.NewValue("rules") + for _, r := range rules { + k.AddValues(strings.Join([]string{r.Role, r.Type, r.Match}, " ")) + } + space.AddKeys(k) + } + } + } + + return +} + +// Rules returns nil +func (app *mercuryEnviron) GetRules(ctx context.Context, id ident.Ident) (lis mercury.Rules, err error) { + identity := id.Identity() + + lis = append(lis, + mercury.Rule{ + Role: "read", + Type: "NS", + Match: mercuryPolicy(identity), + }, + ) + + groups := groups(identity, &app.cfg) + + if u, ok := id.(hasRole); groups.Has("admin") || ok && u.HasRole("admin") { + lis = append(lis, + mercury.Rule{ + Role: "read", + Type: "NS", + Match: mercurySource, + }, + mercury.Rule{ + Role: "read", + Type: "NS", + Match: mercuryPriority, + }, + mercury.Rule{ + Role: "read", + Type: "NS", + Match: mercuryHost, + }, + mercury.Rule{ + Role: "read", + Type: "NS", + Match: appDotEnviron, + }, + ) + } + + return lis, nil +} + +func groups(identity string, cfg *mercury.SpaceMap) set.Set[string] { + groups := set.New[string]() + if s, ok := cfg.Space("mercury.groups"); ok { + for _, g := range s.List { + for _, v := range g.Values { + for _, u := range strings.Fields(v) { + if u == identity { + groups.Add(g.Name) + } + } + } + } + } + return groups +} diff --git a/mercury/http/notify.go b/mercury/http/notify.go new file mode 100644 index 0000000..44cf8e6 --- /dev/null +++ b/mercury/http/notify.go @@ -0,0 +1,63 @@ +package http + +import ( + "bytes" + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "net/http" + + "go.sour.is/pkg/lg" + "go.sour.is/pkg/mercury" +) + +type httpNotify struct{} + +func (httpNotify) SendNotify(ctx context.Context, n mercury.Notify) error { + ctx, span := lg.Span(ctx) + defer span.End() + + cl := &http.Client{} + caCertPool, err := x509.SystemCertPool() + if err != nil { + caCertPool = x509.NewCertPool() + } + + // Setup HTTPS client + tlsConfig := &tls.Config{ + RootCAs: caCertPool, + } + + transport := &http.Transport{TLSClientConfig: tlsConfig} + + cl.Transport = transport + + var req *http.Request + req, err = http.NewRequestWithContext(ctx, n.Method, n.URL, bytes.NewBufferString("")) + if err != nil { + span.RecordError(err) + return err + } + req.Header.Set("content-type", "application/json") + + span.AddEvent(fmt.Sprint("URL: ", n.URL)) + res, err := cl.Do(req) + if err != nil { + span.RecordError(err) + return err + } + res.Body.Close() + span.AddEvent(fmt.Sprint(res.Status)) + if res.StatusCode != 200 { + span.RecordError(err) + err = fmt.Errorf("unable to read config") + return err + } + + return nil +} + +func Register() { + mercury.Registry.Register("http-notify", func(s *mercury.Space) any { return httpNotify{} }) +} diff --git a/mercury/mercury.go b/mercury/mercury.go new file mode 100644 index 0000000..29ce596 --- /dev/null +++ b/mercury/mercury.go @@ -0,0 +1,695 @@ +package mercury + +import ( + "fmt" + "path/filepath" + "strings" + + "golang.org/x/exp/maps" +) + +type Config []*Space + +func NewConfig(spaces ...*Space) Config { + return spaces +} +func (c *Config) AddSpace(spaces ...*Space) *Config { + *c = append(*c, spaces...) + return c +} + +// Len implements Len for sort.interface +func (lis Config) Len() int { + return len(lis) +} + +// Less implements Less for sort.interface +func (lis Config) Less(i, j int) bool { + return lis[i].Space < lis[j].Space +} + +// Swap implements Swap for sort.interface +func (lis Config) Swap(i, j int) { lis[i], lis[j] = lis[j], lis[i] } + +// StringList returns the space names as a list +func (lis Config) StringList() string { + var buf strings.Builder + for _, o := range lis { + if len(o.Notes) > 0 { + buf.WriteString("# ") + buf.WriteString(strings.Join(o.Notes, "\n# ")) + buf.WriteRune('\n') + } + buf.WriteRune('@') + buf.WriteString(o.Space) + if len(o.Tags) > 0 { + buf.WriteRune(' ') + buf.WriteString(strings.Join(o.Tags, " ")) + } + buf.WriteRune('\n') + } + return buf.String() +} + +// ToSpaceMap formats as SpaceMap +func (lis Config) ToSpaceMap() SpaceMap { + out := make(SpaceMap) + for _, c := range lis { + out[c.Space] = c + } + return out +} + +// String format config as string +func (lis Config) String() string { + attLen := 0 + tagLen := 0 + + for _, o := range lis { + for _, v := range o.List { + l := len(v.Name) + if attLen <= l { + attLen = l + } + + t := len(strings.Join(v.Tags, " ")) + if tagLen <= t { + tagLen = t + } + } + } + + var buf strings.Builder + for _, o := range lis { + if len(o.Notes) > 0 { + buf.WriteString("# ") + buf.WriteString(strings.Join(o.Notes, "\n# ")) + buf.WriteRune('\n') + } + + buf.WriteRune('@') + buf.WriteString(o.Space) + if len(o.Tags) > 0 { + buf.WriteRune(' ') + buf.WriteString(strings.Join(o.Tags, " ")) + } + buf.WriteRune('\n') + + for _, v := range o.List { + if len(v.Notes) > 0 { + buf.WriteString("# ") + buf.WriteString(strings.Join(v.Notes, "\n# ")) + buf.WriteString("\n") + } + + buf.WriteString(v.Name) + buf.WriteString(strings.Repeat(" ", attLen-len(v.Name)+1)) + + if len(v.Tags) > 0 { + t := strings.Join(v.Tags, " ") + buf.WriteString(t) + buf.WriteString(strings.Repeat(" ", tagLen-len(t)+1)) + } else { + buf.WriteString(strings.Repeat(" ", tagLen+1)) + } + + switch len(v.Values) { + case 0: + buf.WriteString("\n") + case 1: + buf.WriteString(" :") + buf.WriteString(v.Values[0]) + buf.WriteString("\n") + default: + buf.WriteString(" :") + buf.WriteString(v.Values[0]) + buf.WriteString("\n") + for _, s := range v.Values[1:] { + buf.WriteString(strings.Repeat(" ", attLen+tagLen+3)) + buf.WriteString(":") + buf.WriteString(s) + buf.WriteString("\n") + } + } + } + + buf.WriteRune('\n') + } + + return buf.String() +} + +// EnvString format config as environ +func (lis Config) EnvString() string { + var buf strings.Builder + for _, o := range lis { + for _, v := range o.List { + buf.WriteString(o.Space) + for _, t := range o.Tags { + buf.WriteRune(' ') + buf.WriteString(t) + } + buf.WriteRune(':') + buf.WriteString(v.Name) + for _, t := range v.Tags { + buf.WriteRune(' ') + buf.WriteString(t) + } + switch len(v.Values) { + case 0: + buf.WriteRune('=') + buf.WriteRune('\n') + case 1: + buf.WriteRune('=') + buf.WriteString(v.Values[0]) + buf.WriteRune('\n') + default: + buf.WriteRune('+') + buf.WriteRune('=') + buf.WriteString(v.Values[0]) + buf.WriteRune('\n') + for _, s := range v.Values[1:] { + buf.WriteString(o.Space) + buf.WriteRune(':') + buf.WriteString(v.Name) + buf.WriteRune('+') + buf.WriteRune('=') + buf.WriteString(s) + buf.WriteRune('\n') + } + } + } + } + + return buf.String() +} + +// INIString format config as ini +func (lis Config) INIString() string { + var buf strings.Builder + for _, o := range lis { + buf.WriteRune('[') + buf.WriteString(o.Space) + buf.WriteRune(']') + buf.WriteRune('\n') + for _, v := range o.List { + buf.WriteString(v.Name) + switch len(v.Values) { + case 0: + buf.WriteRune('=') + buf.WriteRune('\n') + case 1: + buf.WriteRune('=') + buf.WriteString(v.Values[0]) + buf.WriteRune('\n') + default: + buf.WriteRune('[') + buf.WriteRune('0') + buf.WriteRune(']') + + buf.WriteRune('=') + buf.WriteString(v.Values[0]) + buf.WriteRune('\n') + for i, s := range v.Values[1:] { + buf.WriteString(v.Name) + buf.WriteRune('[') + buf.WriteString(fmt.Sprintf("%d", i)) + buf.WriteRune(']') + buf.WriteRune('=') + buf.WriteString(s) + buf.WriteRune('\n') + } + } + } + } + + return buf.String() +} + +// String format config as string +func (lis Config) HTMLString() string { + attLen := 0 + tagLen := 0 + + for _, o := range lis { + for _, v := range o.List { + l := len(v.Name) + if attLen <= l { + attLen = l + } + + t := len(strings.Join(v.Tags, " ")) + if tagLen <= t { + tagLen = t + } + } + } + + var buf strings.Builder + for _, o := range lis { + if len(o.Notes) > 0 { + buf.WriteString("") + buf.WriteString("# ") + buf.WriteString(strings.Join(o.Notes, "\n# ")) + buf.WriteString("") + buf.WriteRune('\n') + } + + buf.WriteString("") + buf.WriteRune('@') + buf.WriteString(o.Space) + buf.WriteString("") + if len(o.Tags) > 0 { + buf.WriteRune(' ') + buf.WriteString("") + buf.WriteString(strings.Join(o.Tags, " ")) + buf.WriteString("") + } + buf.WriteRune('\n') + + for _, v := range o.List { + if len(v.Notes) > 0 { + buf.WriteString("") + buf.WriteString("# ") + buf.WriteString(strings.Join(v.Notes, "\n# ")) + buf.WriteString("") + buf.WriteString("\n") + } + + buf.WriteString("") + buf.WriteString(v.Name) + buf.WriteString("") + buf.WriteString(strings.Repeat(" ", attLen-len(v.Name)+1)) + + if len(v.Tags) > 0 { + t := strings.Join(v.Tags, " ") + buf.WriteString("") + buf.WriteString(t) + buf.WriteString(strings.Repeat(" ", tagLen-len(t)+1)) + buf.WriteString("") + } else { + buf.WriteString(strings.Repeat(" ", tagLen+1)) + } + + switch len(v.Values) { + case 0: + buf.WriteString("\n") + case 1: + buf.WriteString(" :") + buf.WriteString(v.Values[0]) + buf.WriteString("\n") + default: + buf.WriteString(" :") + buf.WriteString(v.Values[0]) + buf.WriteString("\n") + for _, s := range v.Values[1:] { + buf.WriteString(strings.Repeat(" ", attLen+tagLen+3)) + buf.WriteString(":") + buf.WriteString(s) + buf.WriteString("\n") + } + } + } + + buf.WriteRune('\n') + } + + return buf.String() +} + +// Space stores a registry of spaces +type Space struct { + Space string `json:"space"` + Tags []string `json:"tags,omitempty"` + Notes []string `json:"notes,omitempty"` + List []Value `json:"list,omitempty"` +} + +func NewSpace(space string) *Space { + return &Space{Space: space} +} + +// HasTag returns true if needle is found +// If the needle ends with a / it will be treated +// as a prefix for tag meta data. +func (s *Space) HasTag(needle string) bool { + isPrefix := strings.HasSuffix(needle, "/") + for i := range s.Tags { + switch isPrefix { + case true: + if strings.HasPrefix(s.Tags[i], needle) { + return true + } + case false: + if s.Tags[i] == needle { + return true + } + } + } + return false +} + +// GetTagMeta retuns the value after a '/' in a tag. +// Tags are in the format 'name' or 'name/value' +// This function returns the value. +func (s *Space) GetTagMeta(needle string, offset int) string { + if !strings.HasSuffix(needle, "/") { + needle += "/" + } + + for i := range s.Tags { + if strings.HasPrefix(s.Tags[i], needle) { + if offset > 0 { + offset-- + continue + } + + return strings.TrimPrefix(s.Tags[i], needle) + } + } + + return "" +} + +// FirstTagMeta returns the first meta tag value. +func (s *Space) FirstTagMeta(needle string) string { + return s.GetTagMeta(needle, 0) +} + +// GetValues that match name +func (s *Space) GetValues(name string) (lis []Value) { + for i := range s.List { + if s.List[i].Name == name { + lis = append(lis, s.List[i]) + } + } + return +} + +// FirstValue that matches name +func (s *Space) FirstValue(name string) Value { + for i := range s.List { + if s.List[i].Name == name { + return s.List[i] + } + } + return Value{} +} + +func (s *Space) SetTags(tags ...string) *Space { + s.Tags = tags + return s +} +func (s *Space) AddTags(tags ...string) *Space { + s.Tags = append(s.Tags, tags...) + return s +} +func (s *Space) SetNotes(notes ...string) *Space { + s.Notes = notes + return s +} +func (s *Space) AddNotes(notes ...string) *Space { + s.Notes = append(s.Notes, notes...) + return s +} +func (s *Space) SetKeys(keys ...*Value) *Space { + for i := range keys { + k := *keys[i] + k.Seq = uint64(i) + s.List = append(s.List, k) + } + + return s +} +func (s *Space) AddKeys(keys ...*Value) *Space { + l := uint64(len(s.List)) + for i := range keys { + k := *keys[i] + k.Seq = uint64(i) + l + s.List = append(s.List, k) + } + return s +} + +// SpaceMap generic map of space values +type SpaceMap map[string]*Space + +func (m SpaceMap) Space(name string) (*Space, bool) { + s, ok := m[name] + return s, ok +} + +// Rule is a type of rule +type Rule struct { + Role string + Type string + Match string +} + +// Rules is a list of rules +type Rules []Rule + +// GetNamespaceSearch returns a default search for users rules. +func (r Rules) GetNamespaceSearch() (lis NamespaceSearch) { + for _, o := range r { + if o.Type == "NS" && (o.Role == "read" || o.Role == "write") { + lis = append(lis, NamespaceStar(o.Match)) + } + } + return +} + +// Check if name matches rule +func (r Rule) Check(name string) bool { + ok, err := filepath.Match(r.Match, name) + if err != nil { + return false + } + return ok +} + +// CheckNamespace verifies user has access +func (r Rules) CheckNamespace(search NamespaceSearch) bool { + for _, ns := range search { + if !r.GetRoles("NS", ns.Value()).HasRole("read", "write") { + return false + } + } + + return true +} + +func (r Rules) Less(i, j int) bool { + si, sj := scoreRule(r[i]), scoreRule(r[j]) + if si != sj { + return si < sj + } + return len(r[i].Match) < len(r[j].Match) +} +func (r Rules) Swap(i, j int) { r[i], r[j] = r[j], r[i] } +func (r Rules) Len() int { return len(r) } + +func scoreRule(r Rule) int { + score := 0 + if r.Type == "GR" { + score += 1000 + } + switch r.Role { + case "admin": + score += 100 + case "write": + score += 50 + case "read": + score += 10 + } + return score +} + +// ReduceSearch verifies user has access +func (r Rules) ReduceSearch(search NamespaceSearch) (out NamespaceSearch) { + rules := r.GetNamespaceSearch() + skip := make(map[string]struct{}) + out = make(NamespaceSearch, 0, len(rules)) + + for _, rule := range rules { + if _, ok := skip[rule.Raw()]; ok { + continue + } + for _, ck := range search { + if _, ok := skip[ck.Raw()]; ok { + continue + } else if rule.Match(ck.Raw()) { + skip[ck.Raw()] = struct{}{} + out = append(out, ck) + } else if ck.Match(rule.Raw()) { + out = append(out, rule) + } + } + } + + return +} + +// Roles is a list of roles for a resource +type Roles map[string]struct{} + +// GetRoles returns a list of Roles +func (r Rules) GetRoles(typ, name string) (lis Roles) { + lis = make(Roles) + for _, o := range r { + if typ == o.Type && o.Check(name) { + lis[o.Role] = struct{}{} + } + } + return +} + +// HasRole is a valid role +func (r Roles) HasRole(roles ...string) bool { + for _, role := range roles { + if _, ok := r[role]; ok { + return true + } + } + return false +} + +// ToArray converts SpaceMap to ArraySpace +func (m SpaceMap) ToArray() Config { + a := make(Config, 0, len(m)) + for _, s := range m { + a = append(a, s) + } + return a +} +func (m *SpaceMap) MergeMap(s SpaceMap) { + m.Merge(maps.Values(s)...) +} +func (m *SpaceMap) Merge(lis ...*Space) { + for _, s := range lis { + // Only accept first version based on priority. + if _, ok := (*m)[s.Space]; ok { + continue + } + + (*m)[s.Space] = s + + // // Merge values together. + // c, ok := (*m)[s.Space] + // if ok { + // c = &Space{} + // } + // c.Notes = append(c.Notes, s.Notes...) + // c.Tags = append(c.Tags, s.Tags...) + // last := c.List[len(c.List)-1].Seq + // for i := range s.List { + // v := s.List[i] + // v.Seq += last + // c.List = append(c.List, v) + // } + // (*m)[s.Space] = c + } +} + +// Value stores the attributes for space values +type Value struct { + Space string `json:"-" db:"space"` + Seq uint64 `json:"-" db:"seq"` + Name string `json:"name"` + Values []string `json:"values"` + Notes []string `json:"notes"` + Tags []string `json:"tags"` +} + +// func (v *Value) ID() string { +// return gql.FmtID("MercurySpace:%v:%v", v.Space, v.Seq) +// } + +// HasTag returns true if needle is found +// If the needle ends with a / it will be treated +// as a prefix for tag meta data. +func (v Value) HasTag(needle string) bool { + isPrefix := strings.HasSuffix(needle, "/") + for i := range v.Tags { + switch isPrefix { + case true: + if strings.HasPrefix(v.Tags[i], needle) { + return true + } + case false: + if v.Tags[i] == needle { + return true + } + } + } + return false +} + +// GetTagMeta retuns the value after a '/' in a tag. +// Tags are in the format 'name' or 'name/value' +// This function returns the value. +func (v Value) GetTagMeta(needle string, offset int) string { + if !strings.HasSuffix(needle, "/") { + needle += "/" + } + + for i := range v.Tags { + if strings.HasPrefix(v.Tags[i], needle) { + if offset > 0 { + offset-- + continue + } + + return strings.TrimPrefix(v.Tags[i], needle) + } + } + + return "" +} + +// FirstTagMeta returns the first meta tag value. +func (v Value) FirstTagMeta(needle string) string { + return v.GetTagMeta(needle, 0) +} + +// First value in array. +func (v Value) First() string { + if len(v.Values) < 1 { + return "" + } + + return v.Values[0] +} + +// Join values with newlines. +func (v Value) Join() string { + return strings.Join(v.Values, "\n") +} + +func NewValue(name string) *Value { + return &Value{Name: name} +} +func (v *Value) SetTags(tags ...string) *Value { + v.Tags = tags + return v +} +func (v *Value) AddTags(tags ...string) *Value { + v.Tags = append(v.Tags, tags...) + return v +} +func (v *Value) SetNotes(notes ...string) *Value { + v.Notes = notes + return v +} +func (v *Value) AddNotes(notes ...string) *Value { + v.Notes = append(v.Notes, notes...) + return v +} +func (v *Value) SetValues(values ...string) *Value { + v.Values = values + return v +} +func (v *Value) AddValues(values ...string) *Value { + v.Values = append(v.Values, values...) + return v +} diff --git a/mercury/mqtt/notify.go b/mercury/mqtt/notify.go new file mode 100644 index 0000000..f67b057 --- /dev/null +++ b/mercury/mqtt/notify.go @@ -0,0 +1,27 @@ +package mqtt + +import ( + "context" + + "go.sour.is/pkg/lg" + "go.sour.is/pkg/mercury" +) + +type mqttNotify struct{} + +func (mqttNotify) SendNotify(ctx context.Context, n mercury.Notify) { + _, span := lg.Span(ctx) + defer span.End() + // var m mqtt.Message + // m, err = mqtt.NewMessage(n.URL, n) + // if err != nil { + // return + // } + // log.Debug(n) + // err = mqtt.Publish(m) + // return +} + +func Register() { + mercury.Registry.Register("mqtt-notify", func(s *mercury.Space) any { return &mqttNotify{} }) +} diff --git a/mercury/namespace.go b/mercury/namespace.go new file mode 100644 index 0000000..c783714 --- /dev/null +++ b/mercury/namespace.go @@ -0,0 +1,125 @@ +package mercury + +import ( + "path/filepath" + "strings" +) + +// NamespaceSpec implements a parsed namespace search +type NamespaceSpec interface { + Value() string + String() string + Raw() string + Match(string) bool +} + +// NamespaceSearch list of namespace specs +type NamespaceSearch []NamespaceSpec + +// ParseNamespace returns a list of parsed values +func ParseNamespace(ns string) (lis NamespaceSearch) { + for _, part := range strings.Split(ns, ";") { + if strings.HasPrefix(part, "trace:") { + for _, s := range strings.Split(part[6:], ",") { + lis = append(lis, NamespaceTrace(s)) + } + } else { + for _, s := range strings.Split(part, ",") { + if strings.Contains(s, "*") { + lis = append(lis, NamespaceStar(s)) + } else { + lis = append(lis, NamespaceNode(s)) + } + } + } + } + + return +} + +// String output string value +func (n NamespaceSearch) String() string { + lis := make([]string, 0, len(n)) + + for _, v := range n { + lis = append(lis, v.String()) + } + return strings.Join(lis, ",") +} + +// Match returns true if any match. +func (n NamespaceSearch) Match(s string) bool { + for _, m := range n { + ok, err := filepath.Match(m.Raw(), s) + if err != nil { + return false + } + if ok { + return true + } + } + + return false +} + +// NamespaceNode implements a node search value +type NamespaceNode string + +// String output string value +func (n NamespaceNode) String() string { return string(n) } + +// Quote return quoted value. +// func (n NamespaceNode) Quote() string { return `'` + n.Value() + `'` } + +// Value to return the value +func (n NamespaceNode) Value() string { return string(n) } + +// Raw return raw value. +func (n NamespaceNode) Raw() string { return string(n) } + +// Match returns true if any match. +func (n NamespaceNode) Match(s string) bool { return match(n, s) } + +// NamespaceTrace implements a trace search value +type NamespaceTrace string + +// String output string value +func (n NamespaceTrace) String() string { return "trace:" + string(n) } + +// Quote return quoted value. +// func (n NamespaceTrace) Quote() string { return `'` + n.Value() + `'` } + +// Value to return the value +func (n NamespaceTrace) Value() string { return strings.Replace(string(n), "*", "%", -1) } + +// Raw return raw value. +func (n NamespaceTrace) Raw() string { return string(n) } + +// Match returns true if any match. +func (n NamespaceTrace) Match(s string) bool { return match(n, s) } + +// NamespaceStar implements a trace search value +type NamespaceStar string + +// String output string value +func (n NamespaceStar) String() string { return string(n) } + +// Quote return quoted value. +// func (n NamespaceStar) Quote() string { return `'` + n.Value() + `'` } + +// Value to return the value +func (n NamespaceStar) Value() string { return strings.Replace(string(n), "*", "%", -1) } + +// Raw return raw value. +func (n NamespaceStar) Raw() string { return string(n) } + +// Match returns true if any match. +func (n NamespaceStar) Match(s string) bool { return match(n, s) } + +func match(n NamespaceSpec, s string) bool { + ok, err := filepath.Match(n.Raw(), s) + if err != nil { + return false + } + return ok +} diff --git a/mercury/parse.go b/mercury/parse.go new file mode 100644 index 0000000..6bcb33d --- /dev/null +++ b/mercury/parse.go @@ -0,0 +1,110 @@ +package mercury + +import ( + "bufio" + "io" + "strings" +) + +func ParseText(body io.Reader) (config SpaceMap, err error) { + config = make(SpaceMap) + + var space string + var name string + var tags []string + var notes []string + var seq uint64 + + scanner := bufio.NewScanner(body) + for scanner.Scan() { + line := scanner.Text() + + if len(line) == 0 { + continue + } + + if strings.HasPrefix(line, "#") { + notes = append(notes, strings.TrimPrefix(line, "# ")) + continue + } + + if strings.HasPrefix(line, "@") { + var c *Space + var ok bool + + sp := strings.Fields(strings.TrimPrefix(line, "@")) + space = sp[0] + + if c, ok = config[space]; !ok { + c = &Space{Space: space} + } + + c.Notes = append(make([]string, 0, len(notes)), notes...) + c.Tags = append(make([]string, 0, len(sp[1:])), sp[1:]...) + + config[space] = c + notes = notes[:0] + tags = tags[:0] + + continue + } + + if space == "" { + continue + } + + sp := strings.SplitN(line, ":", 2) + if len(sp) < 2 { + continue + } + + if strings.TrimSpace(sp[0]) == "" { + var c *Space + var ok bool + + if c, ok = config[space]; !ok { + c = &Space{Space: space} + } + + c.List[len(c.List)-1].Values = append(c.List[len(c.List)-1].Values, sp[1]) + config[space] = c + + continue + } + + fields := strings.Fields(sp[0]) + name = fields[0] + if len(fields) > 1 { + tags = fields[1:] + } + + var c *Space + var ok bool + + if c, ok = config[space]; !ok { + c = &Space{Space: space} + } + + seq++ + c.List = append( + c.List, + Value{ + Seq: seq, + Name: name, + Tags: append(make([]string, 0, len(tags)), tags...), + Notes: append(make([]string, 0, len(notes)), notes...), + Values: []string{sp[1]}, + }, + ) + config[space] = c + + notes = notes[:0] + tags = tags[:0] + } + + if err = scanner.Err(); err != nil { + return nil, err + } + + return +} diff --git a/mercury/public/favicon.ico b/mercury/public/favicon.ico new file mode 100644 index 0000000..30e536c Binary files /dev/null and b/mercury/public/favicon.ico differ diff --git a/mercury/public/index.html b/mercury/public/index.html new file mode 100644 index 0000000..9a020b1 --- /dev/null +++ b/mercury/public/index.html @@ -0,0 +1,47 @@ + + + + +
+