Initial WIP import
5
COPYING
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
Copyright 2022 Erin Shepherd
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
24
README.md
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# Authentricity
|
||||||
|
## A Lightweight Distributed Authentication System
|
||||||
|
Authentricity is a lightweight authenticaton system for distributed environments.
|
||||||
|
Users and groups are stored internally in the systemd JSON
|
||||||
|
[user](https://systemd.io/USER_RECORD/) and [group](https://systemd.io/GROUP_RECORD/)
|
||||||
|
record formats
|
||||||
|
|
||||||
|
Theoretically the storage backends are pluggable, but presently only
|
||||||
|
[Hashicorp Consul](https://www.consul.io/) is supported.
|
||||||
|
|
||||||
|
This project is very much a work in progress
|
||||||
|
|
||||||
|
### Components
|
||||||
|
#### authentricity-hostagent
|
||||||
|
The hostagent should run on every machine for which you wish to use Authentricity for
|
||||||
|
Unix logins. This component implements the systemd
|
||||||
|
[User/Group Varlink API](https://systemd.io/USER_GROUP_API/) to support user and group
|
||||||
|
lookups.
|
||||||
|
|
||||||
|
#### authentricity-webui
|
||||||
|
SSO portal and user administration UI
|
||||||
|
|
||||||
|
#### authentricity-admin
|
||||||
|
Command line administation tool (performing direct database accesses)
|
7
cmd/authentricity-admin/main.go
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "go.e43.eu/authentricity/internal/admintool"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
admintool.Main()
|
||||||
|
}
|
7
cmd/authentricity-hostagent/main.go
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "go.e43.eu/authentricity/internal/hostagent"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
hostagent.Main()
|
||||||
|
}
|
7
cmd/authentricity-webui/main.go
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "go.e43.eu/authentricity/internal/webui"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
webui.Main()
|
||||||
|
}
|
53
go.mod
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
module go.e43.eu/authentricity
|
||||||
|
|
||||||
|
go 1.17
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
|
||||||
|
github.com/coreos/go-systemd/v22 v22.3.2
|
||||||
|
github.com/go-chi/chi/v5 v5.0.7
|
||||||
|
github.com/google/uuid v1.3.0
|
||||||
|
github.com/gorilla/csrf v1.7.1
|
||||||
|
github.com/hashicorp/consul/api v1.13.0
|
||||||
|
github.com/kelseyhightower/envconfig v1.4.0
|
||||||
|
github.com/lestrrat-go/jwx/v2 v2.0.3
|
||||||
|
github.com/manifoldco/promptui v0.9.0
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0
|
||||||
|
github.com/spf13/cobra v1.5.0
|
||||||
|
github.com/varlink/go v0.4.1-0.20211215093235-013d563ae87a
|
||||||
|
github.com/zaffka/zap-to-hclog v0.10.5
|
||||||
|
go.etcd.io/bbolt v1.3.6
|
||||||
|
go.uber.org/zap v1.21.0
|
||||||
|
golang.org/x/sys v0.0.0-20220708085239-5a0f0661e09d
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da // indirect
|
||||||
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
|
||||||
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
|
||||||
|
github.com/fatih/color v1.13.0 // indirect
|
||||||
|
github.com/goccy/go-json v0.9.7 // indirect
|
||||||
|
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.1 // indirect
|
||||||
|
github.com/hashicorp/go-hclog v1.2.1 // indirect
|
||||||
|
github.com/hashicorp/go-immutable-radix v1.0.0 // indirect
|
||||||
|
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
|
||||||
|
github.com/hashicorp/golang-lru v0.5.0 // indirect
|
||||||
|
github.com/hashicorp/serf v0.9.6 // indirect
|
||||||
|
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||||
|
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
|
||||||
|
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||||
|
github.com/lestrrat-go/httprc v1.0.2 // indirect
|
||||||
|
github.com/lestrrat-go/iter v1.0.2 // indirect
|
||||||
|
github.com/lestrrat-go/option v1.0.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
go.uber.org/atomic v1.9.0 // indirect
|
||||||
|
go.uber.org/multierr v1.8.0 // indirect
|
||||||
|
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
replace github.com/varlink/go => github.com/erincandescent/varlink-go v0.4.1-0.20220710172442-b1ca3a35207e
|
253
go.sum
Normal file
|
@ -0,0 +1,253 @@
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 h1:KeNholpO2xKjgaaSyd+DyQRrsQjhbSeS7qe4nEw8aQw=
|
||||||
|
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo=
|
||||||
|
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||||
|
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da h1:8GUt8eRujhVEGZFFEjBj46YV4rDjvGrNxb0KMWYkL2I=
|
||||||
|
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||||
|
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||||
|
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||||
|
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||||
|
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||||
|
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||||
|
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
|
||||||
|
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||||
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
|
||||||
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||||
|
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
|
||||||
|
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||||
|
github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=
|
||||||
|
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||||
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
|
||||||
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
|
||||||
|
github.com/erincandescent/varlink-go v0.4.1-0.20220710172442-b1ca3a35207e h1:vGp1F0lLJqVpuZNVXedply/GckrIcibkVmHoFvQ6KQk=
|
||||||
|
github.com/erincandescent/varlink-go v0.4.1-0.20220710172442-b1ca3a35207e/go.mod h1:DKg9Y2ctoNkesREGAEak58l+jOC6JU2aqZvUYs5DynU=
|
||||||
|
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
|
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||||
|
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||||
|
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||||
|
github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
|
||||||
|
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||||
|
github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM=
|
||||||
|
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw=
|
||||||
|
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
|
github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
|
||||||
|
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
||||||
|
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||||
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gorilla/csrf v1.7.1 h1:Ir3o2c1/Uzj6FBxMlAUB6SivgVMy1ONXwYgXn+/aHPE=
|
||||||
|
github.com/gorilla/csrf v1.7.1/go.mod h1:+a/4tCmqhG6/w4oafeAZ9pEa3/NZOWYVbD9fV0FwIQA=
|
||||||
|
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||||
|
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||||
|
github.com/hashicorp/consul/api v1.13.0 h1:2hnLQ0GjQvw7f3O61jMO8gbasZviZTrt9R8WzgiirHc=
|
||||||
|
github.com/hashicorp/consul/api v1.13.0/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ=
|
||||||
|
github.com/hashicorp/consul/sdk v0.8.0 h1:OJtKBtEjboEZvG6AOUdh4Z1Zbyu0WcxQ0qatRrZHTVU=
|
||||||
|
github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=
|
||||||
|
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
|
||||||
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||||
|
github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
||||||
|
github.com/hashicorp/go-hclog v0.16.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
||||||
|
github.com/hashicorp/go-hclog v1.2.1 h1:YQsLlGDJgwhXFpucSPyVbCBviQtjlHv3jLTlp8YmtEw=
|
||||||
|
github.com/hashicorp/go-hclog v1.2.1/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||||
|
github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0=
|
||||||
|
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||||
|
github.com/hashicorp/go-msgpack v0.5.3 h1:zKjpN5BK/P5lMYrLmBHdBULWbJ0XpYR+7NGzqkZzoD4=
|
||||||
|
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||||
|
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||||
|
github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI=
|
||||||
|
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
|
||||||
|
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
|
||||||
|
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||||
|
github.com/hashicorp/go-sockaddr v1.0.0 h1:GeH6tui99pF4NJgfnhp+L6+FfobzVW3Ah46sLo0ICXs=
|
||||||
|
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||||
|
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||||
|
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
|
github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE=
|
||||||
|
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
|
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
|
||||||
|
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
|
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||||
|
github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
|
||||||
|
github.com/hashicorp/memberlist v0.3.0 h1:8+567mCcFDnS5ADl7lrpxPMWiFCElyUEeW0gtj34fMA=
|
||||||
|
github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
|
||||||
|
github.com/hashicorp/serf v0.9.6 h1:uuEX1kLR6aoda1TBttmJQKDLZE1Ob7KN0NPdE7EtCDc=
|
||||||
|
github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
|
||||||
|
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||||
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
|
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
|
||||||
|
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
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/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
|
||||||
|
github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
|
||||||
|
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
|
||||||
|
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
|
||||||
|
github.com/lestrrat-go/httprc v1.0.2 h1:1hPZNeBRt9cvvYFR83XTkoQQidSFmAj3dqgR3AuQj9U=
|
||||||
|
github.com/lestrrat-go/httprc v1.0.2/go.mod h1:5Ml+nB++j6IC0e6LzefJnrpMQDKgDwDCaIQQzhbqhJM=
|
||||||
|
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
|
||||||
|
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
|
||||||
|
github.com/lestrrat-go/jwx/v2 v2.0.3 h1:9zeZGkbiVkiSuzRsy2SbQJdTuA/At1I2Hh9R/GonCKg=
|
||||||
|
github.com/lestrrat-go/jwx/v2 v2.0.3/go.mod h1:4tnab1l/rJWhxmtVsAtc2kr+pWGg72IcnWFk8gM0tLM=
|
||||||
|
github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4=
|
||||||
|
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||||
|
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
|
||||||
|
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
|
||||||
|
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||||
|
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
|
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
|
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
|
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
||||||
|
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||||
|
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||||
|
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
|
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
|
||||||
|
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||||
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
|
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||||
|
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||||
|
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
|
||||||
|
github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY=
|
||||||
|
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
|
||||||
|
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
|
github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0=
|
||||||
|
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||||
|
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.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
|
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c h1:Lgl0gzECD8GnQ5QCWA8o6BtfL6mDH5rQgM4/fX3avOs=
|
||||||
|
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||||
|
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
|
||||||
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||||
|
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
|
||||||
|
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||||
|
github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
|
||||||
|
github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
|
||||||
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
|
||||||
|
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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
|
||||||
|
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||||
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
|
github.com/zaffka/zap-to-hclog v0.10.5 h1:6s9uMa4H8slD3c0UE7Ga3DreJci95Ujjbc8X+bMVPAU=
|
||||||
|
github.com/zaffka/zap-to-hclog v0.10.5/go.mod h1:5b3vf3ndIbXOmBrnDHoCyh4F6h5VNtTO2va7AX17cwg=
|
||||||
|
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
|
||||||
|
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
|
||||||
|
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||||
|
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
|
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||||
|
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
|
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
|
||||||
|
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||||
|
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
||||||
|
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||||
|
go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8=
|
||||||
|
go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
|
||||||
|
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||||
|
go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
|
||||||
|
go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
|
||||||
|
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc=
|
||||||
|
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
|
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||||
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||||
|
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
|
||||||
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
|
||||||
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220708085239-5a0f0661e09d h1:/m5NbqQelATgoSPVC2Z23sR4kVNokFwDDyWh/3rGY+I=
|
||||||
|
golang.org/x/sys v0.0.0-20220708085239-5a0f0661e09d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
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.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
|
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
71
internal/admintool/group.go
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
package admintool
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/manifoldco/promptui"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"go.e43.eu/authentricity/internal/models"
|
||||||
|
"go.e43.eu/authentricity/internal/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
var createGroupCmd = &cobra.Command{
|
||||||
|
Use: "create",
|
||||||
|
Short: "Interactive group creation tool",
|
||||||
|
RunE: func(_ *cobra.Command, args []string) error {
|
||||||
|
cst, err := store.NewConsulStore(zap.L())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
groupname, err := query("Group Name", validateUsername)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
description, err := query("Description", nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, disp, err := (&promptui.Select{
|
||||||
|
Label: "Disposition",
|
||||||
|
Items: []models.Disposition{
|
||||||
|
models.DispositionSystem,
|
||||||
|
models.DispositionRegular,
|
||||||
|
},
|
||||||
|
}).Run()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
group := &models.GroupRecord{
|
||||||
|
UUID: uuid.New(),
|
||||||
|
GroupName: groupname,
|
||||||
|
Description: description,
|
||||||
|
Disposition: models.Disposition(disp),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = cst.CreateEntity(context.Background(), group)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Created (ID %s)\n", group.ID())
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var groupCmd = &cobra.Command{
|
||||||
|
Use: "group",
|
||||||
|
Short: "Group tools",
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
groupCmd.AddCommand(createGroupCmd)
|
||||||
|
rootCmd.AddCommand(groupCmd)
|
||||||
|
}
|
35
internal/admintool/root.go
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
package admintool
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rootCmd = &cobra.Command{
|
||||||
|
Use: "authentricity-admin",
|
||||||
|
Short: "Admin utilities for Authentricity",
|
||||||
|
}
|
||||||
|
|
||||||
|
func Main() {
|
||||||
|
L, err := zap.NewProduction()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Initializing logger", err)
|
||||||
|
}
|
||||||
|
if os.Getenv("AUTHENTRICITY_DEBUG") != "" {
|
||||||
|
L, err = zap.NewDevelopment()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Initializing logger", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer zap.ReplaceGlobals(L)()
|
||||||
|
defer zap.RedirectStdLog(L)()
|
||||||
|
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
226
internal/admintool/user.go
Normal file
|
@ -0,0 +1,226 @@
|
||||||
|
package admintool
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/GehirnInc/crypt/sha512_crypt"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/manifoldco/promptui"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"go.e43.eu/authentricity/internal/models"
|
||||||
|
"go.e43.eu/authentricity/internal/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
func validateUsername(s string) error {
|
||||||
|
cls, _ := models.ClassifyName(s)
|
||||||
|
if cls != models.NameClassUsername {
|
||||||
|
return errors.New("Invalid username")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateEmail(s string) error {
|
||||||
|
if s == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cls, _ := models.ClassifyName(s)
|
||||||
|
if cls != models.NameClassEmail {
|
||||||
|
return errors.New("Invalid email")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func query(label string, validator func(string) error) (string, error) {
|
||||||
|
p := promptui.Prompt{
|
||||||
|
Label: label,
|
||||||
|
Validate: validator,
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryPassword(label string) (string, error) {
|
||||||
|
p := promptui.Prompt{
|
||||||
|
Label: label,
|
||||||
|
Mask: '*',
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
var createUserCmd = &cobra.Command{
|
||||||
|
Use: "create",
|
||||||
|
Short: "Interactive user creation tool",
|
||||||
|
RunE: func(_ *cobra.Command, args []string) error {
|
||||||
|
cst, err := store.NewConsulStore(zap.L())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
username, err := query("Username", validateUsername)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
email, err := query("Email (Optional)", validateEmail)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
realName, err := query("'Real' Name (Optional)", nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
password, err := queryPassword("Password (Optional)")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var sshKeys []string
|
||||||
|
for {
|
||||||
|
key, err := query("SSH Key (Optional)", nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if key == "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
sshKeys = append(sshKeys, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
user := &models.UserRecord{
|
||||||
|
UUID: uuid.New(),
|
||||||
|
UserName: username,
|
||||||
|
EmailAddress: email,
|
||||||
|
RealName: realName,
|
||||||
|
Privileged: &models.UserPrivileged{
|
||||||
|
SSHAuthorizedKeys: sshKeys,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if password != "" {
|
||||||
|
c := sha512_crypt.New()
|
||||||
|
hash, err := c.Generate([]byte(password), nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
user.Privileged.HashedPassword = []string{hash}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = cst.CreateEntity(context.Background(), user)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Created (ID %s)\n", user.ID())
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var userAddTogroupCmd = &cobra.Command{
|
||||||
|
Use: "add-to-group [user] [group]",
|
||||||
|
Short: "Add a user to a group",
|
||||||
|
RunE: func(_ *cobra.Command, args []string) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
cst, err := store.NewConsulStore(zap.L())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) != 2 {
|
||||||
|
return errors.New("Need two parameters")
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, err := store.GetEntityIDByAnyName(ctx, cst, args[0])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("looking up user ID: %w", err)
|
||||||
|
}
|
||||||
|
groupID, err := store.GetEntityIDByAnyName(ctx, cst, args[1])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("looking up group ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
user, _, err := cst.GetEntity(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("looking up user: %w", err)
|
||||||
|
} else if user == nil {
|
||||||
|
return fmt.Errorf("user '%s' not found", args[0])
|
||||||
|
} else if user.Type() != models.TypeUser {
|
||||||
|
return fmt.Errorf("Expected a user, got a %s", user.Type())
|
||||||
|
}
|
||||||
|
|
||||||
|
group, _, err := cst.GetEntity(ctx, groupID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("looking up group: %w", err)
|
||||||
|
} else if group == nil {
|
||||||
|
return fmt.Errorf("group '%s' not found", args[1])
|
||||||
|
} else if group.Type() != models.TypeGroup {
|
||||||
|
return fmt.Errorf("Expected a group, got a %s", user.Type())
|
||||||
|
}
|
||||||
|
|
||||||
|
return cst.AddUserToGroup(ctx, userID, groupID)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var userRemoveFromGroupCmd = &cobra.Command{
|
||||||
|
Use: "remove-from-group [user] [group]",
|
||||||
|
Short: "Remove user from a group",
|
||||||
|
RunE: func(_ *cobra.Command, args []string) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
cst, err := store.NewConsulStore(zap.L())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) != 2 {
|
||||||
|
return errors.New("Need two parameters")
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, err := store.GetEntityIDByAnyName(ctx, cst, args[0])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("looking up user ID: %w", err)
|
||||||
|
}
|
||||||
|
groupID, err := store.GetEntityIDByAnyName(ctx, cst, args[1])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("looking up group ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
user, _, err := cst.GetEntity(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("looking up user: %w", err)
|
||||||
|
} else if user == nil {
|
||||||
|
return fmt.Errorf("user '%s' not found", args[0])
|
||||||
|
} else if user.Type() != models.TypeUser {
|
||||||
|
return fmt.Errorf("Expected a user, got a %s", user.Type())
|
||||||
|
}
|
||||||
|
|
||||||
|
group, _, err := cst.GetEntity(ctx, groupID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("looking up group: %w", err)
|
||||||
|
} else if group == nil {
|
||||||
|
return fmt.Errorf("group '%s' not found", args[1])
|
||||||
|
} else if group.Type() != models.TypeGroup {
|
||||||
|
return fmt.Errorf("Expected a group, got a %s", user.Type())
|
||||||
|
}
|
||||||
|
|
||||||
|
return cst.RemoveUserFromGroup(ctx, userID, groupID)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var userCmd = &cobra.Command{
|
||||||
|
Use: "user",
|
||||||
|
Short: "User tools",
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
userCmd.AddCommand(createUserCmd)
|
||||||
|
userCmd.AddCommand(userAddTogroupCmd)
|
||||||
|
userCmd.AddCommand(userRemoveFromGroupCmd)
|
||||||
|
|
||||||
|
rootCmd.AddCommand(userCmd)
|
||||||
|
}
|
75
internal/boltx/boltx.go
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
package boltx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
bolt "go.etcd.io/bbolt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DB struct {
|
||||||
|
*bolt.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tx struct {
|
||||||
|
*bolt.Tx
|
||||||
|
}
|
||||||
|
|
||||||
|
type txKey *DB
|
||||||
|
|
||||||
|
func OpenDB(path string) (*DB, error) {
|
||||||
|
db, err := bolt.Open(path, 0700, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &DB{db}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) begin(writable bool) (*Tx, error) {
|
||||||
|
btx, err := d.DB.Begin(writable)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Tx{btx}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) withTx(ctx context.Context, writable bool, fn func(context.Context, *Tx) error) (err error) {
|
||||||
|
tx, ok := ctx.Value(txKey(d)).(*Tx)
|
||||||
|
if !ok {
|
||||||
|
tx, err = d.begin(writable)
|
||||||
|
defer func() {
|
||||||
|
if !writable || err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
} else {
|
||||||
|
err = tx.Commit()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
ctx = context.WithValue(ctx, txKey(d), tx)
|
||||||
|
} else {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
err = fn(ctx, tx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) WithView(ctx context.Context, fn func(context.Context) error) error {
|
||||||
|
return d.withTx(ctx, false, func(ctx context.Context, tx *Tx) error {
|
||||||
|
return fn(ctx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) WithUpdate(ctx context.Context, fn func(context.Context) error) error {
|
||||||
|
return d.withTx(ctx, true, func(ctx context.Context, tx *Tx) error {
|
||||||
|
return fn(ctx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) InView(ctx context.Context, fn func(context.Context, *Tx) error) error {
|
||||||
|
return d.withTx(ctx, false, fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) InUpdate(ctx context.Context, fn func(context.Context, *Tx) error) error {
|
||||||
|
return d.withTx(ctx, true, fn)
|
||||||
|
}
|
11
internal/boltx/errors.go
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
package boltx
|
||||||
|
|
||||||
|
type constErr string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ErrRequiresWritableTransaction constErr = "Attempt to execute mutation method in read-only transaction"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e constErr) Error() string {
|
||||||
|
return string(e)
|
||||||
|
}
|
149
internal/hostagent/bindings/db.go
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
package bindings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"go.e43.eu/authentricity/internal/boltx"
|
||||||
|
bolt "go.etcd.io/bbolt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DB struct {
|
||||||
|
db *boltx.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
uuidToUnixBucket = []byte("binding_by_uuid")
|
||||||
|
unixToUUIDBucket = []byte("binding_by_uxid")
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewDB(db *boltx.DB, MinUXID uint64) (*DB, error) {
|
||||||
|
if err := db.Update(func(tx *bolt.Tx) error {
|
||||||
|
forwardBucket, err := tx.CreateBucketIfNotExists(uuidToUnixBucket)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = tx.CreateBucketIfNotExists(unixToUUIDBucket)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if forwardBucket.Sequence() < MinUXID {
|
||||||
|
forwardBucket.SetSequence(MinUXID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &DB{db}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBindingByUUID returns a binding by its UUID. Returns 0 if no binding exists
|
||||||
|
func (d *DB) GetBindingByUUID(ctx context.Context, id uuid.UUID) (ugid uint32, err error) {
|
||||||
|
err = d.db.InView(ctx, func(ctx context.Context, tx *boltx.Tx) error {
|
||||||
|
b := tx.Bucket(uuidToUnixBucket)
|
||||||
|
|
||||||
|
val := b.Get(id[:])
|
||||||
|
switch len(val) {
|
||||||
|
case 0:
|
||||||
|
ugid = 0
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case 4:
|
||||||
|
ugid = binary.BigEndian.Uint32(val)
|
||||||
|
return nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid binding: got %x, expected 4 byte Unix ID", val)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBindingByUUID returns a binding by its Unix ID. Returns a nil UUID if no binding exists
|
||||||
|
func (d *DB) GetBindingByUnixID(ctx context.Context, uxid uint32) (uxuuid uuid.UUID, err error) {
|
||||||
|
err = d.db.InView(ctx, func(ctx context.Context, tx *boltx.Tx) (err error) {
|
||||||
|
b := tx.Bucket(unixToUUIDBucket)
|
||||||
|
|
||||||
|
var k [4]byte
|
||||||
|
binary.BigEndian.PutUint32(k[:], uxid)
|
||||||
|
|
||||||
|
val := b.Get(k[:])
|
||||||
|
switch len(val) {
|
||||||
|
case 0:
|
||||||
|
uxuuid = uuid.UUID{}
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case 16:
|
||||||
|
uxuuid, err = uuid.FromBytes(val)
|
||||||
|
return err
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid binding: got %x, expected 16 byte UUID", val)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureBindingByUUID retrieves the binding from a UUID to Unix ID. If no such ID exists, one
|
||||||
|
// is created
|
||||||
|
func (d *DB) EnsureBindingByUUID(ctx context.Context, id uuid.UUID) (ugid uint32, err error) {
|
||||||
|
err = d.db.InUpdate(ctx, func(ctx context.Context, tx *boltx.Tx) (err error) {
|
||||||
|
forwardBucket := tx.Bucket(uuidToUnixBucket)
|
||||||
|
|
||||||
|
val := forwardBucket.Get(id[:])
|
||||||
|
switch len(val) {
|
||||||
|
case 0:
|
||||||
|
id64, err := forwardBucket.NextSequence()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if id64 > math.MaxUint32 {
|
||||||
|
return fmt.Errorf("Next sequence %d out of range for unix IDs", id64)
|
||||||
|
}
|
||||||
|
|
||||||
|
ugid = uint32(id64)
|
||||||
|
var uxid [4]byte
|
||||||
|
binary.BigEndian.PutUint32(uxid[:], ugid)
|
||||||
|
|
||||||
|
reverseBucket := tx.Bucket(unixToUUIDBucket)
|
||||||
|
if err = reverseBucket.Put(uxid[:], id[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return forwardBucket.Put(id[:], uxid[:])
|
||||||
|
|
||||||
|
case 4:
|
||||||
|
ugid = binary.BigEndian.Uint32(val)
|
||||||
|
return nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid binding: got %x, expected 4 bytes", val)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) ForEach(ctx context.Context, fn func(context.Context, uuid.UUID, uint32) error) error {
|
||||||
|
return d.db.InView(ctx, func(ctx context.Context, tx *boltx.Tx) (err error) {
|
||||||
|
reverseBucket := tx.Bucket(unixToUUIDBucket)
|
||||||
|
return reverseBucket.ForEach(func(k, v []byte) error {
|
||||||
|
if len(k) != 4 {
|
||||||
|
return fmt.Errorf("invalid binding: got %x, expected 4 byte Unix ID", k)
|
||||||
|
} else if len(v) != 16 {
|
||||||
|
return fmt.Errorf("invalid binding: got %x, expected 16 byte UUID", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
ugid := binary.BigEndian.Uint32(k)
|
||||||
|
uuid, err := uuid.FromBytes(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fn(ctx, uuid, ugid)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
111
internal/hostagent/hostagent.go
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
package hostagent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/kelseyhightower/envconfig"
|
||||||
|
"github.com/varlink/go/varlink"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"go.e43.eu/authentricity/internal/boltx"
|
||||||
|
"go.e43.eu/authentricity/internal/hostagent/bindings"
|
||||||
|
"go.e43.eu/authentricity/internal/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Debug bool `default:"false"`
|
||||||
|
DataDir string `envconfig:"data_dir" default:"/var/lib/authentricity/hostagent"`
|
||||||
|
SocketPath string `envconfig:"socket_path" default:"/var/run/authentricity/hostagent.sock"`
|
||||||
|
MinUnixID uint32 `envconfig:"min_unix_id" default:"2000"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Main() {
|
||||||
|
var (
|
||||||
|
cfg Config
|
||||||
|
L *zap.Logger
|
||||||
|
)
|
||||||
|
|
||||||
|
err := envconfig.Process("authentricity_hostagent", &cfg)
|
||||||
|
if err != nil {
|
||||||
|
envconfig.Usage("authentricity_hostagent", &cfg)
|
||||||
|
log.Fatal("Parsing settings: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Debug {
|
||||||
|
L, err = zap.NewDevelopment()
|
||||||
|
} else {
|
||||||
|
L, err = zap.NewProduction()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Initializing logger", err)
|
||||||
|
}
|
||||||
|
defer zap.ReplaceGlobals(L)()
|
||||||
|
defer zap.RedirectStdLog(L)()
|
||||||
|
S := L.Sugar()
|
||||||
|
|
||||||
|
if err := os.MkdirAll(cfg.DataDir, 0700); err != nil {
|
||||||
|
S.Fatalf("Error creating hostagent data directory '%s': %v", cfg.DataDir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
socketDir := path.Dir(cfg.SocketPath)
|
||||||
|
if err := os.MkdirAll(socketDir, 0755); err != nil {
|
||||||
|
S.Fatalf("Error creating hostagent socket directory '%s': %v", socketDir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := boltx.OpenDB(path.Join(cfg.DataDir, "local.db"))
|
||||||
|
if err != nil {
|
||||||
|
S.Fatalf("Error opening agent database: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bindingDB, err := bindings.NewDB(db, uint64(cfg.MinUnixID))
|
||||||
|
if err != nil {
|
||||||
|
S.Fatalf("Error opening bindings database: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cst, err := store.NewConsulStore(L.Named("consul_store"))
|
||||||
|
if err != nil {
|
||||||
|
S.Fatalf("Error creating Consul store: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
st, err := store.NewCachedStore(cst, db, &store.CachedStoreConfig{
|
||||||
|
Logger: L.Named("cached_store"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
S.Fatalf("Error creating cached store: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
svc, err := varlink.NewService(
|
||||||
|
"Authentricity Project",
|
||||||
|
"Authentricity Host Agent",
|
||||||
|
"Unknown Version",
|
||||||
|
"http://authentricity.e43.eu/")
|
||||||
|
if err != nil {
|
||||||
|
S.Fatalf("Error creating varlink service: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = svc.RegisterInterface(newUserDBService(st, bindingDB))
|
||||||
|
if err != nil {
|
||||||
|
S.Fatalf("Error registering io.systemd.UserDatabase service: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = svc.RegisterInterface(newHostagentService(st, bindingDB))
|
||||||
|
if err != nil {
|
||||||
|
S.Fatalf("Error registering eu.e43.authentricity.HostAgent service: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = svc.Bind(ctx, "unix:"+cfg.SocketPath)
|
||||||
|
if err != nil {
|
||||||
|
S.Fatalf("Error binding varlink address '%s': %v", "unix:"+cfg.SocketPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
S.Infof("Listening for requests on socket %s", cfg.SocketPath)
|
||||||
|
err = svc.DoListen(ctx, 0)
|
||||||
|
if !errors.Is(err, context.Canceled) {
|
||||||
|
S.Fatalf("Error serving requests: %v", err)
|
||||||
|
}
|
||||||
|
}
|
15
internal/hostagent/priv.go
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
package hostagent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/varlink/go/varlink"
|
||||||
|
"go.e43.eu/authentricity/internal/peercred"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AllowPrivileged(conn varlink.ReadWriterContext, uid uint32) bool {
|
||||||
|
creds := peercred.Get(conn)
|
||||||
|
if creds == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return creds.Uid == 0 || creds.Uid == uid
|
||||||
|
}
|
297
internal/hostagent/varlink_hostagent.go
Normal file
|
@ -0,0 +1,297 @@
|
||||||
|
package hostagent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"go.e43.eu/authentricity/internal/hostagent/bindings"
|
||||||
|
"go.e43.eu/authentricity/internal/store"
|
||||||
|
"go.e43.eu/authentricity/internal/varlink/eue43authentricityhostagent"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HostagentService struct {
|
||||||
|
store store.Store
|
||||||
|
bindings *bindings.DB
|
||||||
|
logger *zap.SugaredLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHostagentService(
|
||||||
|
store store.Store,
|
||||||
|
bindings *bindings.DB,
|
||||||
|
) *eue43authentricityhostagent.VarlinkInterface {
|
||||||
|
return eue43authentricityhostagent.VarlinkNew(&HostagentService{
|
||||||
|
store: store,
|
||||||
|
bindings: bindings,
|
||||||
|
logger: zap.S().Named("eu.e43.authentricity.HostAgent"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *HostagentService) GetEntity(
|
||||||
|
ctx context.Context,
|
||||||
|
c eue43authentricityhostagent.VarlinkCall,
|
||||||
|
unixId_ *int64,
|
||||||
|
name_ *string,
|
||||||
|
email_ *string,
|
||||||
|
uuid_ *string,
|
||||||
|
) error {
|
||||||
|
var ids []uuid.UUID
|
||||||
|
if unixId_ != nil {
|
||||||
|
id, err := svc.bindings.GetBindingByUnixID(ctx, uint32(*unixId_))
|
||||||
|
if err != nil {
|
||||||
|
return c.ReplyServiceNotAvailable(ctx, err.Error())
|
||||||
|
} else if id == uuid.Nil {
|
||||||
|
return c.ReplyNoRecordFound(ctx)
|
||||||
|
}
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if name_ != nil {
|
||||||
|
id, _, err := svc.store.GetEntityIDByName(ctx, *name_)
|
||||||
|
if err != nil {
|
||||||
|
return c.ReplyServiceNotAvailable(ctx, err.Error())
|
||||||
|
} else if id == uuid.Nil {
|
||||||
|
return c.ReplyNoRecordFound(ctx)
|
||||||
|
}
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if email_ != nil {
|
||||||
|
id, _, err := svc.store.GetEntityIDByEmail(ctx, *email_)
|
||||||
|
if err != nil {
|
||||||
|
return c.ReplyServiceNotAvailable(ctx, err.Error())
|
||||||
|
} else if id == uuid.Nil {
|
||||||
|
return c.ReplyNoRecordFound(ctx)
|
||||||
|
}
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if uuid_ != nil {
|
||||||
|
id, err := uuid.Parse(*uuid_)
|
||||||
|
if err != nil || id == uuid.Nil {
|
||||||
|
return c.ReplyInvalidParameter(ctx, "uuid")
|
||||||
|
}
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch len(ids) {
|
||||||
|
case 0:
|
||||||
|
return c.ReplyNoRecordFound(ctx)
|
||||||
|
default:
|
||||||
|
for _, v := range ids[1:] {
|
||||||
|
if v != ids[0] {
|
||||||
|
return c.ReplyConflictingRecordFound(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ent, _, err := svc.store.GetEntity(ctx, ids[0])
|
||||||
|
if err != nil {
|
||||||
|
return c.ReplyServiceNotAvailable(ctx, err.Error())
|
||||||
|
} else if ent == nil {
|
||||||
|
return c.ReplyNoRecordFound(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
ent.StripPrivileged()
|
||||||
|
body, err := json.Marshal(ent)
|
||||||
|
if err != nil {
|
||||||
|
return c.ReplyServiceNotAvailable(ctx, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.ReplyGetEntity(ctx, json.RawMessage(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *HostagentService) GetEntityID(
|
||||||
|
ctx context.Context,
|
||||||
|
c eue43authentricityhostagent.VarlinkCall,
|
||||||
|
unixId_ *int64,
|
||||||
|
name_ *string,
|
||||||
|
email_ *string,
|
||||||
|
) error {
|
||||||
|
var ids []uuid.UUID
|
||||||
|
if unixId_ != nil {
|
||||||
|
id, err := svc.bindings.GetBindingByUnixID(ctx, uint32(*unixId_))
|
||||||
|
if err != nil {
|
||||||
|
return c.ReplyServiceNotAvailable(ctx, err.Error())
|
||||||
|
} else if id == uuid.Nil {
|
||||||
|
return c.ReplyNoRecordFound(ctx)
|
||||||
|
}
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if name_ != nil {
|
||||||
|
id, _, err := svc.store.GetEntityIDByName(ctx, *name_)
|
||||||
|
if err != nil {
|
||||||
|
return c.ReplyServiceNotAvailable(ctx, err.Error())
|
||||||
|
} else if id == uuid.Nil {
|
||||||
|
return c.ReplyNoRecordFound(ctx)
|
||||||
|
}
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if email_ != nil {
|
||||||
|
id, _, err := svc.store.GetEntityIDByEmail(ctx, *email_)
|
||||||
|
if err != nil {
|
||||||
|
return c.ReplyServiceNotAvailable(ctx, err.Error())
|
||||||
|
} else if id == uuid.Nil {
|
||||||
|
return c.ReplyNoRecordFound(ctx)
|
||||||
|
}
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch len(ids) {
|
||||||
|
case 0:
|
||||||
|
return c.ReplyNoRecordFound(ctx)
|
||||||
|
case 1:
|
||||||
|
return c.ReplyGetEntityID(ctx, ids[0].String())
|
||||||
|
default:
|
||||||
|
for _, v := range ids[1:] {
|
||||||
|
if v != ids[0] {
|
||||||
|
return c.ReplyConflictingRecordFound(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c.ReplyGetEntityID(ctx, ids[0].String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Based upon a UUID, lookup the corresponding Unix ID
|
||||||
|
// If unixID is absent, enumerates all bindings
|
||||||
|
func (svc *HostagentService) GetUnixID(ctx context.Context, c eue43authentricityhostagent.VarlinkCall, uuid_ *string) error {
|
||||||
|
if uuid_ != nil {
|
||||||
|
id, err := uuid.Parse(*uuid_)
|
||||||
|
if err != nil {
|
||||||
|
return c.ReplyInvalidParameter(ctx, "uuid")
|
||||||
|
}
|
||||||
|
|
||||||
|
uxid, err := svc.bindings.GetBindingByUUID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return c.ReplyServiceNotAvailable(ctx, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no binding exists, check if an object exists. Only if an object
|
||||||
|
// exists with the specified UUID will we create an entry - otherwise
|
||||||
|
// anyone on the system would be able to exhaust our ID pool by continually
|
||||||
|
// asking about made-up UUIDs
|
||||||
|
if uxid == 0 {
|
||||||
|
ent, _, err := svc.store.GetEntity(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return c.ReplyServiceNotAvailable(ctx, err.Error())
|
||||||
|
} else if ent == nil {
|
||||||
|
return c.ReplyNoRecordFound(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
uxid, err = svc.bindings.EnsureBindingByUUID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return c.ReplyServiceNotAvailable(ctx, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c.ReplyGetUnixID(ctx, id.String(), int64(uxid))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.WantsMore() {
|
||||||
|
return c.ReplyMoreRequired(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
hasPrev bool = false
|
||||||
|
prevUUID uuid.UUID
|
||||||
|
prevUXID uint32
|
||||||
|
)
|
||||||
|
err := svc.bindings.ForEach(ctx, func(ctx context.Context, u uuid.UUID, ux uint32) error {
|
||||||
|
var err error
|
||||||
|
if hasPrev {
|
||||||
|
c.Continues = true
|
||||||
|
err = c.ReplyGetUnixID(ctx, prevUUID.String(), int64(prevUXID))
|
||||||
|
}
|
||||||
|
|
||||||
|
hasPrev = true
|
||||||
|
prevUUID = u
|
||||||
|
prevUXID = ux
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return c.ReplyServiceNotAvailable(ctx, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasPrev {
|
||||||
|
c.Continues = false
|
||||||
|
return c.ReplyGetUnixID(ctx, prevUUID.String(), int64(prevUXID))
|
||||||
|
} else {
|
||||||
|
return c.ReplyNoRecordFound(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup the IDs of all groups that a user belongs to
|
||||||
|
func (svc *HostagentService) GetUserGroups(ctx context.Context, c eue43authentricityhostagent.VarlinkCall, userUuid string) error {
|
||||||
|
id, err := uuid.Parse(userUuid)
|
||||||
|
if err != nil {
|
||||||
|
return c.ReplyInvalidParameter(ctx, "userUuid")
|
||||||
|
}
|
||||||
|
|
||||||
|
groupIDs, _, err := svc.store.GetUserGroups(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return c.ReplyServiceNotAvailable(ctx, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(groupIDs) == 0 {
|
||||||
|
return c.ReplyNoRecordFound(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, groupID := range groupIDs {
|
||||||
|
c.Continues = i != len(groupIDs)-1
|
||||||
|
if err := c.ReplyGetUserGroups(ctx, groupID.String()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup the IDs of all groups that a user belongs to
|
||||||
|
func (svc *HostagentService) GetGroupUsers(ctx context.Context, c eue43authentricityhostagent.VarlinkCall, groupUuid string) error {
|
||||||
|
id, err := uuid.Parse(groupUuid)
|
||||||
|
if err != nil {
|
||||||
|
return c.ReplyInvalidParameter(ctx, "userUuid")
|
||||||
|
}
|
||||||
|
|
||||||
|
userIDs, _, err := svc.store.GetGroupUsers(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return c.ReplyServiceNotAvailable(ctx, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(userIDs) == 0 {
|
||||||
|
return c.ReplyNoRecordFound(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, userID := range userIDs {
|
||||||
|
c.Continues = i != len(userIDs)-1
|
||||||
|
if err := c.ReplyGetGroupUsers(ctx, userID.String()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-fetch the information for a specific entity from the source
|
||||||
|
func (svc *HostagentService) Refetch(ctx context.Context, c eue43authentricityhostagent.VarlinkCall, entityId string) error {
|
||||||
|
id, err := uuid.Parse(entityId)
|
||||||
|
if err != nil {
|
||||||
|
return c.ReplyInvalidParameter(ctx, "uuid")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = store.WithMustRefetch(ctx)
|
||||||
|
ent, _, err := svc.store.GetEntity(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return c.ReplyServiceNotAvailable(ctx, err.Error())
|
||||||
|
} else if ent == nil {
|
||||||
|
return c.ReplyNoRecordFound(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
ent.StripPrivileged()
|
||||||
|
body, err := json.Marshal(ent)
|
||||||
|
if err != nil {
|
||||||
|
return c.ReplyServiceNotAvailable(ctx, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.ReplyRefetch(ctx, json.RawMessage(body))
|
||||||
|
}
|
353
internal/hostagent/varlink_sd_userdb.go
Normal file
|
@ -0,0 +1,353 @@
|
||||||
|
package hostagent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"go.e43.eu/authentricity/internal/hostagent/bindings"
|
||||||
|
"go.e43.eu/authentricity/internal/models"
|
||||||
|
"go.e43.eu/authentricity/internal/store"
|
||||||
|
"go.e43.eu/authentricity/internal/varlink/iosystemduserdatabase"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
const USERDB_SERVICE = "eu.e43.authentricity"
|
||||||
|
|
||||||
|
type SystemdUserDBService struct {
|
||||||
|
store store.Store
|
||||||
|
bindings *bindings.DB
|
||||||
|
logger *zap.SugaredLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUserDBService(
|
||||||
|
store store.Store,
|
||||||
|
bindings *bindings.DB,
|
||||||
|
) *iosystemduserdatabase.VarlinkInterface {
|
||||||
|
return iosystemduserdatabase.VarlinkNew(&SystemdUserDBService{
|
||||||
|
store: store,
|
||||||
|
bindings: bindings,
|
||||||
|
logger: zap.S().Named("io.systemd.UserDatabase"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *SystemdUserDBService) lookupIDByName(ctx context.Context, name string) (id uuid.UUID, err error) {
|
||||||
|
return store.GetEntityIDByAnyName(ctx, svc.store, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *SystemdUserDBService) GetUserRecord(
|
||||||
|
ctx context.Context,
|
||||||
|
c iosystemduserdatabase.VarlinkCall,
|
||||||
|
uid *int64,
|
||||||
|
userName *string,
|
||||||
|
service string,
|
||||||
|
) error {
|
||||||
|
if service != USERDB_SERVICE {
|
||||||
|
return c.ReplyBadService(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
id uuid.UUID
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
// If userName is given, we always do forward lookup via name.
|
||||||
|
// This ensures we correctly handle aliases
|
||||||
|
if userName != nil {
|
||||||
|
id, err = svc.lookupIDByName(ctx, *userName)
|
||||||
|
if err != nil {
|
||||||
|
svc.logger.Errorw("Upstream error", "error", err)
|
||||||
|
return c.ReplyServiceNotAvailable(ctx)
|
||||||
|
}
|
||||||
|
} else if uid != nil {
|
||||||
|
id, err = svc.bindings.GetBindingByUnixID(ctx, uint32(*uid))
|
||||||
|
if err != nil {
|
||||||
|
svc.logger.Errorw("Binding lookup error", "error", err)
|
||||||
|
return c.ReplyServiceNotAvailable(ctx)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return c.ReplyEnumerationNotSupported(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if id == uuid.Nil {
|
||||||
|
return c.ReplyNoRecordFound(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
ent, _, err := svc.store.GetEntity(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
svc.logger.Errorw("Upstream error", "error", err)
|
||||||
|
return c.ReplyServiceNotAvailable(ctx)
|
||||||
|
} else if ent == nil {
|
||||||
|
return c.ReplyNoRecordFound(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ent.Type() != models.TypeUser {
|
||||||
|
svc.logger.Errorw("Unexpected entity type '%s' looking up user", ent.Type())
|
||||||
|
return c.ReplyNoRecordFound(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
trueUXID, err := svc.bindings.EnsureBindingByUUID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
svc.logger.Errorw("Binding lookup error", "error", err)
|
||||||
|
return c.ReplyServiceNotAvailable(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if uid != nil && int64(trueUXID) != *uid {
|
||||||
|
return c.ReplyConflictingRecordFound(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
user := ent.(*models.UserRecord)
|
||||||
|
user.UID = trueUXID
|
||||||
|
user.GID = trueUXID
|
||||||
|
user.Service = USERDB_SERVICE
|
||||||
|
|
||||||
|
priv := AllowPrivileged(c.Conn, trueUXID)
|
||||||
|
if !priv {
|
||||||
|
user.StripPrivileged()
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := json.Marshal(user)
|
||||||
|
if err != nil {
|
||||||
|
svc.logger.Errorw("Error marshaling record", "error", err)
|
||||||
|
return c.ReplyServiceNotAvailable(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.ReplyGetUserRecord(ctx, json.RawMessage(body), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *SystemdUserDBService) GetGroupRecord(
|
||||||
|
ctx context.Context,
|
||||||
|
c iosystemduserdatabase.VarlinkCall,
|
||||||
|
gid *int64,
|
||||||
|
groupName *string,
|
||||||
|
service string,
|
||||||
|
) error {
|
||||||
|
if service != USERDB_SERVICE {
|
||||||
|
return c.ReplyBadService(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
id uuid.UUID
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
// If groupName is given, we always do forward lookup via name.
|
||||||
|
// This ensures we correctly handle aliases
|
||||||
|
if groupName != nil {
|
||||||
|
id, err = svc.lookupIDByName(ctx, *groupName)
|
||||||
|
if err != nil {
|
||||||
|
svc.logger.Errorw("Upstream error", "error", err)
|
||||||
|
return c.ReplyServiceNotAvailable(ctx)
|
||||||
|
}
|
||||||
|
} else if gid != nil {
|
||||||
|
id, err = svc.bindings.GetBindingByUnixID(ctx, uint32(*gid))
|
||||||
|
if err != nil {
|
||||||
|
svc.logger.Errorw("Binding lookup error", "error", err)
|
||||||
|
return c.ReplyServiceNotAvailable(ctx)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return c.ReplyEnumerationNotSupported(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if id == uuid.Nil {
|
||||||
|
return c.ReplyNoRecordFound(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
ent, _, err := svc.store.GetEntity(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
svc.logger.Errorw("Upstream error", "error", err)
|
||||||
|
return c.ReplyServiceNotAvailable(ctx)
|
||||||
|
} else if ent == nil {
|
||||||
|
return c.ReplyNoRecordFound(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
var grp *models.GroupRecord
|
||||||
|
switch ent.Type() {
|
||||||
|
case models.TypeUser:
|
||||||
|
grp = ent.(*models.UserRecord).SynthesizeGroup()
|
||||||
|
case models.TypeGroup:
|
||||||
|
grp = ent.(*models.GroupRecord)
|
||||||
|
default:
|
||||||
|
svc.logger.Errorw("Unexpected entity type '%s' looking up group", ent.Type())
|
||||||
|
return c.ReplyNoRecordFound(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We only return "regular" groups - this allows system groups to be managed traditionally,
|
||||||
|
// but also allows us to insert users into them
|
||||||
|
switch grp.Disposition {
|
||||||
|
case "", models.DispositionRegular:
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
svc.logger.Infow("Suppressing group record with irregular disposition",
|
||||||
|
"groupID", grp.UUID,
|
||||||
|
"disposition", grp.Disposition)
|
||||||
|
return c.ReplyNoRecordFound(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
trueUXID, err := svc.bindings.EnsureBindingByUUID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
svc.logger.Errorw("Binding lookup error", "error", err)
|
||||||
|
return c.ReplyServiceNotAvailable(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if gid != nil && int64(trueUXID) != *gid {
|
||||||
|
return c.ReplyConflictingRecordFound(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
grp.GID = trueUXID
|
||||||
|
grp.Service = USERDB_SERVICE
|
||||||
|
|
||||||
|
priv := AllowPrivileged(c.Conn, trueUXID)
|
||||||
|
if !priv {
|
||||||
|
grp.StripPrivileged()
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := json.Marshal(grp)
|
||||||
|
if err != nil {
|
||||||
|
svc.logger.Errorw("Error marshaling record", "error", err)
|
||||||
|
return c.ReplyServiceNotAvailable(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.ReplyGetGroupRecord(ctx, json.RawMessage(body), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *SystemdUserDBService) GetMemberships(
|
||||||
|
ctx context.Context,
|
||||||
|
c iosystemduserdatabase.VarlinkCall,
|
||||||
|
userName *string,
|
||||||
|
groupName *string,
|
||||||
|
service string,
|
||||||
|
) error {
|
||||||
|
if service != USERDB_SERVICE {
|
||||||
|
return c.ReplyBadService(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case userName != nil && groupName != nil:
|
||||||
|
userID, err := svc.lookupIDByName(ctx, *userName)
|
||||||
|
if err != nil {
|
||||||
|
svc.logger.Errorw("Upstream error getting user ID", "error", err)
|
||||||
|
return c.ReplyServiceNotAvailable(ctx)
|
||||||
|
} else if userID == uuid.Nil {
|
||||||
|
return c.ReplyNoRecordFound(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
groupID, err := svc.lookupIDByName(ctx, *groupName)
|
||||||
|
if err != nil {
|
||||||
|
svc.logger.Errorw("Upstream error getting group ID", "error", err)
|
||||||
|
return c.ReplyServiceNotAvailable(ctx)
|
||||||
|
} else if groupID == uuid.Nil {
|
||||||
|
return c.ReplyNoRecordFound(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
groups, _, err := svc.store.GetUserGroups(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
svc.logger.Errorw("Upstream error getting user's groups", "error", err)
|
||||||
|
return c.ReplyServiceNotAvailable(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, grid := range groups {
|
||||||
|
if grid == groupID {
|
||||||
|
return c.ReplyGetMemberships(ctx, *userName, *groupName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c.ReplyNoRecordFound(ctx)
|
||||||
|
|
||||||
|
case userName != nil:
|
||||||
|
if !c.WantsMore() {
|
||||||
|
return c.ReplyError(ctx, "eu.e43.authentricity.MoreRequired", struct{}{})
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, err := svc.lookupIDByName(ctx, *userName)
|
||||||
|
if err != nil {
|
||||||
|
svc.logger.Errorw("Upstream error getting user ID", "error", err)
|
||||||
|
return c.ReplyServiceNotAvailable(ctx)
|
||||||
|
} else if userID == uuid.Nil {
|
||||||
|
return c.ReplyNoRecordFound(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
groupIDs, _, err := svc.store.GetUserGroups(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
svc.logger.Errorw("Upstream error getting user's groups", "error", err)
|
||||||
|
return c.ReplyServiceNotAvailable(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
svc.logger.Debugf("User %s is a member of %d groups", userID, len(groupIDs))
|
||||||
|
|
||||||
|
if len(groupIDs) == 0 {
|
||||||
|
return c.ReplyNoRecordFound(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, groupID := range groupIDs {
|
||||||
|
thisGroupName := groupID.String()
|
||||||
|
|
||||||
|
ent, _, err := svc.store.GetEntity(ctx, groupID)
|
||||||
|
if err != nil {
|
||||||
|
svc.logger.Errorw("Upstream error getting group", "error", err, "id", groupID)
|
||||||
|
} else if ent != nil {
|
||||||
|
thisGroupName = ent.Name()
|
||||||
|
switch ent.Type() {
|
||||||
|
case models.TypeUser:
|
||||||
|
thisGroupName = ent.(*models.UserRecord).UserName
|
||||||
|
case models.TypeGroup:
|
||||||
|
thisGroupName = ent.(*models.GroupRecord).GroupName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Continues = i != len(groupIDs)-1
|
||||||
|
if err := c.ReplyGetMemberships(ctx, *userName, thisGroupName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case groupName != nil:
|
||||||
|
if !c.WantsMore() {
|
||||||
|
return c.ReplyError(ctx, "eu.e43.authentricity.MoreRequired", struct{}{})
|
||||||
|
}
|
||||||
|
|
||||||
|
groupID, err := svc.lookupIDByName(ctx, *groupName)
|
||||||
|
if err != nil {
|
||||||
|
svc.logger.Errorw("Upstream error getting group ID", "error", err)
|
||||||
|
return c.ReplyServiceNotAvailable(ctx)
|
||||||
|
} else if groupID == uuid.Nil {
|
||||||
|
return c.ReplyNoRecordFound(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
userIDs, _, err := svc.store.GetGroupUsers(ctx, groupID)
|
||||||
|
if err != nil {
|
||||||
|
svc.logger.Errorw("Upstream error getting group's users", "error", err)
|
||||||
|
return c.ReplyServiceNotAvailable(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
svc.logger.Debugf("Group %s contains %d users", groupID, len(userIDs))
|
||||||
|
|
||||||
|
if len(userIDs) == 0 {
|
||||||
|
return c.ReplyNoRecordFound(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, userID := range userIDs {
|
||||||
|
thisUserName := userID.String()
|
||||||
|
|
||||||
|
ent, _, err := svc.store.GetEntity(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
svc.logger.Errorw("Upstream error getting user", "error", err, "id", userID)
|
||||||
|
} else if ent != nil {
|
||||||
|
switch ent.Type() {
|
||||||
|
case models.TypeUser:
|
||||||
|
thisUserName = ent.(*models.UserRecord).UserName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Continues = i != len(userIDs)-1
|
||||||
|
if err := c.ReplyGetMemberships(ctx, thisUserName, *groupName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return c.ReplyEnumerationNotSupported(ctx)
|
||||||
|
}
|
||||||
|
}
|
96
internal/models/entity.go
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/mitchellh/mapstructure"
|
||||||
|
)
|
||||||
|
|
||||||
|
func decode(src, dst interface{}) error {
|
||||||
|
d, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
||||||
|
DecodeHook: mapstructure.TextUnmarshallerHookFunc(),
|
||||||
|
Result: dst,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return d.Decode(src)
|
||||||
|
}
|
||||||
|
|
||||||
|
type EntityType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
TypeUser EntityType = "user"
|
||||||
|
TypeGroup EntityType = "group"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Entity interface {
|
||||||
|
ID() uuid.UUID
|
||||||
|
SetID(uuid.UUID)
|
||||||
|
Type() EntityType
|
||||||
|
Name() string
|
||||||
|
Email() string
|
||||||
|
StripPrivileged()
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnknownEntity struct {
|
||||||
|
UUID uuid.UUID `mapstructure:"uuid"`
|
||||||
|
EntityType EntityType `mapstructure:"-"`
|
||||||
|
Other map[string]interface{} `mapstructure:",remain"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ue *UnknownEntity) ID() uuid.UUID {
|
||||||
|
return ue.UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ue *UnknownEntity) SetID(id uuid.UUID) {
|
||||||
|
ue.UUID = id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ue *UnknownEntity) Type() EntityType {
|
||||||
|
return ue.EntityType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ue *UnknownEntity) Name() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ue *UnknownEntity) Email() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ue *UnknownEntity) StripPrivileged() {
|
||||||
|
delete(ue.Other, "privileged")
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEntityOfType(t EntityType) Entity {
|
||||||
|
switch t {
|
||||||
|
case TypeUser:
|
||||||
|
return new(UserRecord)
|
||||||
|
case TypeGroup:
|
||||||
|
return new(GroupRecord)
|
||||||
|
default:
|
||||||
|
return &UnknownEntity{
|
||||||
|
EntityType: t,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseEntityJSON(buf []byte) (Entity, error) {
|
||||||
|
m := make(map[string]interface{})
|
||||||
|
if err := json.Unmarshal(buf, &m); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
t, ok := m["@type"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("Entity lacks @type")
|
||||||
|
}
|
||||||
|
|
||||||
|
ent := NewEntityOfType(EntityType(t))
|
||||||
|
delete(m, "@type")
|
||||||
|
|
||||||
|
return ent, decode(m, ent)
|
||||||
|
}
|
11
internal/models/errors.go
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
type constErr string
|
||||||
|
|
||||||
|
func (e constErr) Error() string {
|
||||||
|
return string(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
ErrInvalidName constErr = "authentricity: Invalid name"
|
||||||
|
)
|
63
internal/models/group.go
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GroupRecord struct {
|
||||||
|
UUID uuid.UUID `mapstructure:"uuid"`
|
||||||
|
GroupName string `mapstructure:"groupName"`
|
||||||
|
Realm string `mapstructure:"realm,omitempty"`
|
||||||
|
Description string `mapstructure:"description,omitempty"`
|
||||||
|
Disposition Disposition `mapstructure:"disposition,omitempty"`
|
||||||
|
GID uint32 `mapstructure:"gid,omitempty"`
|
||||||
|
Service string `mapstructure:"service,omitempty"`
|
||||||
|
Other map[string]interface{} `mapstructure:"-,remain"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gr *GroupRecord) ID() uuid.UUID {
|
||||||
|
return gr.UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gr *GroupRecord) SetID(id uuid.UUID) {
|
||||||
|
gr.UUID = id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gr *GroupRecord) Type() EntityType {
|
||||||
|
return TypeGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gr *GroupRecord) Name() string {
|
||||||
|
return gr.GroupName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gr *GroupRecord) Email() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gr *GroupRecord) StripPrivileged() {
|
||||||
|
delete(gr.Other, "privileged")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gr *GroupRecord) MarshalJSON() ([]byte, error) {
|
||||||
|
m := make(map[string]interface{})
|
||||||
|
for k, v := range gr.Other {
|
||||||
|
m[k] = v
|
||||||
|
}
|
||||||
|
if err := decode(gr, &m); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
m["@type"] = string(TypeGroup)
|
||||||
|
return json.Marshal(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gr *GroupRecord) UnmarshalJSON(data []byte) error {
|
||||||
|
m := make(map[string]interface{})
|
||||||
|
if err := json.Unmarshal(data, m); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
delete(m, "@type")
|
||||||
|
return decode(m, gr)
|
||||||
|
}
|
41
internal/models/names.go
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Strict Mode RE from https://systemd.io/USER_NAMES/,
|
||||||
|
// which seems about as sensible as anything
|
||||||
|
usernameRe = regexp.MustCompile("^[a-zA-Z_][a-zA-Z0-9_-]{0,30}$")
|
||||||
|
)
|
||||||
|
|
||||||
|
type NameClass int
|
||||||
|
|
||||||
|
const (
|
||||||
|
nameClassUnknown NameClass = iota
|
||||||
|
NameClassUUID
|
||||||
|
NameClassUsername
|
||||||
|
NameClassEmail
|
||||||
|
)
|
||||||
|
|
||||||
|
func ClassifyName(name string) (NameClass, error) {
|
||||||
|
if usernameRe.MatchString(name) {
|
||||||
|
return NameClassUsername, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := uuid.Parse(name)
|
||||||
|
if err == nil {
|
||||||
|
return NameClassUUID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(name, "@") {
|
||||||
|
return NameClassEmail, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nameClassUnknown, fmt.Errorf("%w '%s'", ErrInvalidName, name)
|
||||||
|
}
|
166
internal/models/user.go
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/GehirnInc/crypt"
|
||||||
|
_ "github.com/GehirnInc/crypt/sha256_crypt"
|
||||||
|
_ "github.com/GehirnInc/crypt/sha512_crypt"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Disposition string
|
||||||
|
|
||||||
|
const (
|
||||||
|
DispositionIntrinsic Disposition = "intrinsic"
|
||||||
|
DispositionSystem Disposition = "system"
|
||||||
|
DispositionDynamic Disposition = "dynamic"
|
||||||
|
DispositionRegular Disposition = "regular"
|
||||||
|
DispositionContainer Disposition = "container"
|
||||||
|
DispositionReserved Disposition = "reserved"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ResourceLimit struct {
|
||||||
|
Cur uint64 `mapstructure:"cur"`
|
||||||
|
Max uint64 `mapstructure:"max"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserRecord struct {
|
||||||
|
UUID uuid.UUID `mapstructure:"uuid"`
|
||||||
|
UserName string `mapstructure:"userName"`
|
||||||
|
Realm string `mapstructure:"realm,omitempty"`
|
||||||
|
RealName string `mapstructure:"realName,omitempty"`
|
||||||
|
EmailAddress string `mapstructure:"emailAddress,omitempty"`
|
||||||
|
IconName string `mapstructure:"iconName,omitempty"`
|
||||||
|
Location string `mapstructure:"location,omitempty"`
|
||||||
|
Disposition Disposition `mapstructure:"disposition,omitempty"`
|
||||||
|
Shell string `mapstructure:"shell,omitempty"`
|
||||||
|
Ummask *int32 `mapstructure:"umask,omitempty"`
|
||||||
|
Environment []string `mapstructure:"environment,omitempty"`
|
||||||
|
TimeZone string `mapstructure:"timeZone,omitempty"`
|
||||||
|
PreferredLanguage string `mapstructure:"preferredLanguage,omitempty"`
|
||||||
|
NiceLevel int32 `mapstructure:"niceLevel,omitempty"`
|
||||||
|
ResourceLimits map[string]ResourceLimit `mapstructure:"resourceLimits,omitempty"`
|
||||||
|
Locked bool `mapstructure:"locked,omitempty"`
|
||||||
|
NotBeforeUSec *int64 `mapstructure:"notBeforeUSec,omitempty"`
|
||||||
|
NotAfterUSec *int64 `mapstructure:"notAfterUSec,omitempty"`
|
||||||
|
HomeDirectory string `mapstructure:"homeDirectory,omitempty"`
|
||||||
|
UID uint32 `mapstructure:"uid,omitempty"`
|
||||||
|
GID uint32 `mapstructure:"gid,omitempty"`
|
||||||
|
Service string `mapstructure:"service,omitempty"`
|
||||||
|
Privileged *UserPrivileged `mapstructure:"privileged,omitempty"`
|
||||||
|
Other map[string]interface{} `mapstructure:"-,remain"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ur *UserRecord) ID() uuid.UUID {
|
||||||
|
return ur.UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ur *UserRecord) SetID(id uuid.UUID) {
|
||||||
|
ur.UUID = id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ur *UserRecord) Type() EntityType {
|
||||||
|
return TypeUser
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ur *UserRecord) Name() string {
|
||||||
|
return ur.UserName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ur *UserRecord) Email() string {
|
||||||
|
return ur.EmailAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ur *UserRecord) StripPrivileged() {
|
||||||
|
ur.Privileged = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ur *UserRecord) MarshalJSON() ([]byte, error) {
|
||||||
|
m := make(map[string]interface{})
|
||||||
|
for k, v := range ur.Other {
|
||||||
|
m[k] = v
|
||||||
|
}
|
||||||
|
if err := decode(ur, &m); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
m["@type"] = string(TypeUser)
|
||||||
|
return json.Marshal(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ur *UserRecord) UnmarshalJSON(data []byte) error {
|
||||||
|
m := make(map[string]interface{})
|
||||||
|
if err := json.Unmarshal(data, m); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
delete(m, "@type")
|
||||||
|
return decode(m, ur)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ur *UserRecord) SynthesizeGroup() *GroupRecord {
|
||||||
|
return &GroupRecord{
|
||||||
|
UUID: ur.UUID,
|
||||||
|
GroupName: ur.UserName,
|
||||||
|
Realm: ur.Realm,
|
||||||
|
Description: ur.RealName,
|
||||||
|
Disposition: ur.Disposition,
|
||||||
|
GID: ur.GID,
|
||||||
|
Service: ur.Service,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ur *UserRecord) IsUserAllowedToLogin() bool {
|
||||||
|
if ur.Locked {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
nowMicros := time.Now().UnixMicro()
|
||||||
|
if ur.NotBeforeUSec != nil && *ur.NotBeforeUSec > nowMicros {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if ur.NotAfterUSec != nil && *ur.NotAfterUSec < nowMicros {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ur *UserRecord) EnsurePrivileged() *UserPrivileged {
|
||||||
|
if ur.Privileged == nil {
|
||||||
|
ur.Privileged = new(UserPrivileged)
|
||||||
|
}
|
||||||
|
return ur.Privileged
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserPrivileged struct {
|
||||||
|
HashedPassword []string `mapstructure:"hashedPassword,omitempty"`
|
||||||
|
SSHAuthorizedKeys []string `mapstructure:"sshAuthorizedKeys,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (priv *UserPrivileged) CheckPassword(pw string) bool {
|
||||||
|
if priv == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
k := []byte(pw)
|
||||||
|
|
||||||
|
for _, possibleHash := range priv.HashedPassword {
|
||||||
|
if !crypt.IsHashSupported(possibleHash) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
cr := crypt.NewFromHash(possibleHash)
|
||||||
|
err := cr.Verify(possibleHash, k)
|
||||||
|
if err == nil {
|
||||||
|
return true
|
||||||
|
} else if errors.Is(err, crypt.ErrKeyMismatch) {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
55
internal/peercred/peercred.go
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
package peercred
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/varlink/go/varlink"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
type getSyscallConn interface {
|
||||||
|
SyscallConn() (syscall.RawConn, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Get(conn varlink.ReadWriterContext) *unix.Ucred {
|
||||||
|
gnc, ok := conn.(varlink.GetNetConn)
|
||||||
|
if !ok {
|
||||||
|
fmt.Println("Not GetNetConn")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
nc := gnc.NetConn()
|
||||||
|
if nc == nil {
|
||||||
|
fmt.Println("No NetConn")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
gsc, ok := nc.(getSyscallConn)
|
||||||
|
if !ok {
|
||||||
|
fmt.Println("Not GetSyscallConn")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
raw, err := gsc.SyscallConn()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Getting syscall conn", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var cred *unix.Ucred
|
||||||
|
err2 := raw.Control(func(fd uintptr) {
|
||||||
|
cred, err = unix.GetsockoptUcred(int(fd),
|
||||||
|
unix.SOL_SOCKET,
|
||||||
|
unix.SO_PEERCRED)
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil || err2 != nil {
|
||||||
|
fmt.Println(err, err2)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Peercred ", cred)
|
||||||
|
|
||||||
|
return cred
|
||||||
|
}
|
455
internal/store/cached_store.go
Normal file
|
@ -0,0 +1,455 @@
|
||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"go.e43.eu/authentricity/internal/boltx"
|
||||||
|
"go.e43.eu/authentricity/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
errCacheEntryTooSmall constErr = "Cache entry too small"
|
||||||
|
errCacheEntryVersionUnknown constErr = "Cache entry version unknown"
|
||||||
|
errUUIDArraySizeWrong constErr = "UUID array not a multiple of 16 bytes"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
entityBucket = []byte("cached_entities")
|
||||||
|
nameBucket = []byte("cache_by_name")
|
||||||
|
emailBucket = []byte("cache_by_email")
|
||||||
|
userGroupsBucket = []byte("cache_user_groups")
|
||||||
|
groupUsersBucket = []byte("cache_group_users")
|
||||||
|
)
|
||||||
|
|
||||||
|
type cacheEntry struct {
|
||||||
|
Metadata EntryMetadata
|
||||||
|
Body []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ce cacheEntry) MarshalBinary() ([]byte, error) {
|
||||||
|
buf := make([]byte, 24, 24+len(ce.Body))
|
||||||
|
buf[0] = 0 // Format version
|
||||||
|
binary.BigEndian.PutUint64(buf[8:], uint64(ce.Metadata.LastFetched.UnixMilli()))
|
||||||
|
binary.BigEndian.PutUint64(buf[16:], ce.Metadata.Revision)
|
||||||
|
return append(buf, ce.Body...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ce *cacheEntry) UnmarshalBinary(buf []byte) error {
|
||||||
|
switch {
|
||||||
|
case len(buf) < 24:
|
||||||
|
return errCacheEntryTooSmall
|
||||||
|
case buf[0] != 0:
|
||||||
|
return fmt.Errorf("%w (version %d)", errCacheEntryVersionUnknown, buf[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
ce.Metadata.LastFetched = time.UnixMilli(int64(binary.BigEndian.Uint64(buf[8:])))
|
||||||
|
ce.Metadata.Revision = binary.BigEndian.Uint64(buf[16:])
|
||||||
|
ce.Body = append([]byte{}, buf[24:]...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type altCacheEntry struct {
|
||||||
|
cacheEntry
|
||||||
|
Bucket []byte
|
||||||
|
Key []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type CachedStoreConfig struct {
|
||||||
|
MaxAge time.Duration
|
||||||
|
MaxNegativeEntryAge time.Duration
|
||||||
|
Logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
type cachedStore struct {
|
||||||
|
upstream Store
|
||||||
|
db *boltx.DB
|
||||||
|
maxAge time.Duration
|
||||||
|
maxNegativeEntryAge time.Duration
|
||||||
|
logger *zap.SugaredLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCachedStore(
|
||||||
|
upstream Store,
|
||||||
|
db *boltx.DB,
|
||||||
|
config *CachedStoreConfig,
|
||||||
|
) (Store, error) {
|
||||||
|
s := &cachedStore{
|
||||||
|
upstream: upstream,
|
||||||
|
db: db,
|
||||||
|
maxAge: 60 * time.Second,
|
||||||
|
maxNegativeEntryAge: time.Second,
|
||||||
|
logger: zap.S(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if config != nil {
|
||||||
|
if config.MaxAge != 0 {
|
||||||
|
s.maxAge = config.MaxAge
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Logger != nil {
|
||||||
|
s.logger = config.Logger.Sugar()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.db.InUpdate(context.Background(), func(ctx context.Context, tx *boltx.Tx) error {
|
||||||
|
if _, err := tx.CreateBucketIfNotExists(entityBucket); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := tx.CreateBucketIfNotExists(nameBucket); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := tx.CreateBucketIfNotExists(emailBucket); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := tx.CreateBucketIfNotExists(userGroupsBucket); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := tx.CreateBucketIfNotExists(groupUsersBucket); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *cachedStore) freshEnough(ce *cacheEntry) bool {
|
||||||
|
if ce == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
maxAge := s.maxAge
|
||||||
|
if len(ce.Body) == 0 {
|
||||||
|
maxAge = s.maxNegativeEntryAge
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Now().Sub(ce.Metadata.LastFetched) < maxAge
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *cachedStore) cacheFetch(ctx context.Context, bucket, key []byte) (ce *cacheEntry, err error) {
|
||||||
|
err = s.db.InView(ctx, func(ctx context.Context, tx *boltx.Tx) error {
|
||||||
|
val := tx.Bucket(bucket).Get(key)
|
||||||
|
if len(val) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ce = new(cacheEntry)
|
||||||
|
return ce.UnmarshalBinary(val)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *cachedStore) upstreamFetch(
|
||||||
|
ctx context.Context,
|
||||||
|
bucket, key []byte,
|
||||||
|
fetchFn func(ctx context.Context) (*cacheEntry, []altCacheEntry, error),
|
||||||
|
) (ce *cacheEntry, err error) {
|
||||||
|
var aces []altCacheEntry
|
||||||
|
ce, aces, err = fetchFn(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
blob, err := ce.MarshalBinary()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.db.InUpdate(ctx, func(ctx context.Context, tx *boltx.Tx) error {
|
||||||
|
if err := tx.Bucket(bucket).Put(key, blob); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ace := range aces {
|
||||||
|
blob, err := ace.MarshalBinary()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = tx.Bucket(ace.Bucket).Put(ace.Key, blob)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ce, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *cachedStore) fetch(
|
||||||
|
ctx context.Context,
|
||||||
|
bucket, key []byte,
|
||||||
|
decodeFn func(data []byte) (interface{}, error),
|
||||||
|
fetchFn func(ctx context.Context) (*cacheEntry, []altCacheEntry, error),
|
||||||
|
) (interface{}, EntryMetadata, error) {
|
||||||
|
var (
|
||||||
|
ce *cacheEntry
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
mustRefetch := MustRefetch(ctx)
|
||||||
|
|
||||||
|
if !mustRefetch {
|
||||||
|
ce, err = s.cacheFetch(ctx, bucket, key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, EntryMetadata{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ce == nil || !s.freshEnough(ce) {
|
||||||
|
newCe, err := s.upstreamFetch(ctx, bucket, key, fetchFn)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Errorf("Fetch failed with upstream: %v", err.Error())
|
||||||
|
|
||||||
|
// If we have nothing cached *or* we're doing a forced refetch, just
|
||||||
|
// return the error and bail here
|
||||||
|
if ce == nil {
|
||||||
|
return nil, EntryMetadata{}, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ce = newCe
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ent, err := decodeFn(ce.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, EntryMetadata{}, err
|
||||||
|
}
|
||||||
|
return ent, ce.Metadata, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *cachedStore) GetEntity(ctx context.Context, id uuid.UUID) (models.Entity, EntryMetadata, error) {
|
||||||
|
ent, md, err := s.fetch(
|
||||||
|
ctx,
|
||||||
|
entityBucket,
|
||||||
|
id[:],
|
||||||
|
func(data []byte) (interface{}, error) {
|
||||||
|
return models.ParseEntityJSON(data)
|
||||||
|
},
|
||||||
|
func(ctx context.Context) (*cacheEntry, []altCacheEntry, error) {
|
||||||
|
ent, md, err := s.upstream.GetEntity(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
body []byte
|
||||||
|
aces []altCacheEntry
|
||||||
|
)
|
||||||
|
|
||||||
|
if ent != nil {
|
||||||
|
body, err = json.Marshal(ent)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if ent.Name() != "" {
|
||||||
|
aces = append(aces, altCacheEntry{
|
||||||
|
Bucket: nameBucket,
|
||||||
|
Key: []byte(ent.Name()),
|
||||||
|
cacheEntry: cacheEntry{
|
||||||
|
Metadata: md,
|
||||||
|
Body: id[:],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if ent.Email() != "" {
|
||||||
|
aces = append(aces, altCacheEntry{
|
||||||
|
Bucket: emailBucket,
|
||||||
|
Key: []byte(ent.Email()),
|
||||||
|
cacheEntry: cacheEntry{
|
||||||
|
Metadata: md,
|
||||||
|
Body: id[:],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &cacheEntry{
|
||||||
|
Metadata: md,
|
||||||
|
Body: body,
|
||||||
|
}, aces, nil
|
||||||
|
})
|
||||||
|
if ent != nil {
|
||||||
|
return ent.(models.Entity), md, err
|
||||||
|
} else {
|
||||||
|
return nil, md, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeUUID(data []byte) (interface{}, error) {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return uuid.Nil, nil
|
||||||
|
} else {
|
||||||
|
return uuid.FromBytes(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeUUID(id uuid.UUID) []byte {
|
||||||
|
if id == uuid.Nil {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return id[:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *cachedStore) GetEntityIDByName(ctx context.Context, name string) (uuid.UUID, EntryMetadata, error) {
|
||||||
|
ent, md, err := s.fetch(
|
||||||
|
ctx,
|
||||||
|
nameBucket,
|
||||||
|
[]byte(name),
|
||||||
|
decodeUUID,
|
||||||
|
func(ctx context.Context) (*cacheEntry, []altCacheEntry, error) {
|
||||||
|
id, md, err := s.upstream.GetEntityIDByName(ctx, name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &cacheEntry{
|
||||||
|
Metadata: md,
|
||||||
|
Body: encodeUUID(id),
|
||||||
|
}, nil, nil
|
||||||
|
})
|
||||||
|
if ent != nil {
|
||||||
|
return ent.(uuid.UUID), md, err
|
||||||
|
} else {
|
||||||
|
return uuid.UUID{}, md, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *cachedStore) GetEntityIDByEmail(ctx context.Context, email string) (uuid.UUID, EntryMetadata, error) {
|
||||||
|
ent, md, err := s.fetch(
|
||||||
|
ctx,
|
||||||
|
emailBucket,
|
||||||
|
[]byte(email),
|
||||||
|
decodeUUID,
|
||||||
|
func(ctx context.Context) (*cacheEntry, []altCacheEntry, error) {
|
||||||
|
id, md, err := s.upstream.GetEntityIDByEmail(ctx, email)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &cacheEntry{
|
||||||
|
Metadata: md,
|
||||||
|
Body: encodeUUID(id),
|
||||||
|
}, nil, nil
|
||||||
|
})
|
||||||
|
if ent != nil {
|
||||||
|
return ent.(uuid.UUID), md, err
|
||||||
|
} else {
|
||||||
|
return uuid.UUID{}, md, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeUUIDs(data []byte) (interface{}, error) {
|
||||||
|
if (len(data) % 16) != 0 {
|
||||||
|
return nil, errUUIDArraySizeWrong
|
||||||
|
}
|
||||||
|
|
||||||
|
ids := make([]uuid.UUID, len(data)/16)
|
||||||
|
for i := range ids {
|
||||||
|
off := i * 16
|
||||||
|
id, err := uuid.FromBytes(data[off : off+16])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ids[i] = id
|
||||||
|
}
|
||||||
|
return ids, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeUUIDs(ids []uuid.UUID) []byte {
|
||||||
|
buf := make([]byte, 0, 16*len(ids))
|
||||||
|
|
||||||
|
for _, id := range ids {
|
||||||
|
buf = append(buf, id[:]...)
|
||||||
|
}
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *cachedStore) GetUserGroups(ctx context.Context, userID uuid.UUID) ([]uuid.UUID, EntryMetadata, error) {
|
||||||
|
ent, md, err := s.fetch(
|
||||||
|
ctx,
|
||||||
|
userGroupsBucket,
|
||||||
|
userID[:],
|
||||||
|
decodeUUIDs,
|
||||||
|
func(ctx context.Context) (*cacheEntry, []altCacheEntry, error) {
|
||||||
|
ids, md, err := s.upstream.GetUserGroups(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &cacheEntry{
|
||||||
|
Metadata: md,
|
||||||
|
Body: encodeUUIDs(ids),
|
||||||
|
}, nil, nil
|
||||||
|
})
|
||||||
|
if ent != nil {
|
||||||
|
return ent.([]uuid.UUID), md, err
|
||||||
|
} else {
|
||||||
|
return nil, md, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *cachedStore) GetGroupUsers(ctx context.Context, groupID uuid.UUID) ([]uuid.UUID, EntryMetadata, error) {
|
||||||
|
ent, md, err := s.fetch(
|
||||||
|
ctx,
|
||||||
|
groupUsersBucket,
|
||||||
|
groupID[:],
|
||||||
|
decodeUUIDs,
|
||||||
|
func(ctx context.Context) (*cacheEntry, []altCacheEntry, error) {
|
||||||
|
ids, md, err := s.upstream.GetGroupUsers(ctx, groupID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &cacheEntry{
|
||||||
|
Metadata: md,
|
||||||
|
Body: encodeUUIDs(ids),
|
||||||
|
}, nil, nil
|
||||||
|
})
|
||||||
|
if ent != nil {
|
||||||
|
return ent.([]uuid.UUID), md, err
|
||||||
|
} else {
|
||||||
|
return nil, md, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Composite operations
|
||||||
|
func (s *cachedStore) GetEntityByName(ctx context.Context, name string) (
|
||||||
|
ent models.Entity,
|
||||||
|
md EntryMetadata,
|
||||||
|
err error,
|
||||||
|
) {
|
||||||
|
id, md, err := s.GetEntityIDByName(ctx, name)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.GetEntity(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *cachedStore) GetEntityByEmail(ctx context.Context, name string) (
|
||||||
|
ent models.Entity,
|
||||||
|
md EntryMetadata,
|
||||||
|
err error,
|
||||||
|
) {
|
||||||
|
id, md, err := s.GetEntityIDByEmail(ctx, name)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.GetEntity(ctx, id)
|
||||||
|
}
|
365
internal/store/consul_store.go
Normal file
|
@ -0,0 +1,365 @@
|
||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/hashicorp/consul/api"
|
||||||
|
consul "github.com/hashicorp/consul/api"
|
||||||
|
wrapper "github.com/zaffka/zap-to-hclog"
|
||||||
|
"go.e43.eu/authentricity/internal/models"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Leading path components for different kinds of data
|
||||||
|
const (
|
||||||
|
pathEntities = "E"
|
||||||
|
pathNames = "n"
|
||||||
|
pathEmails = "@"
|
||||||
|
pathUserMemberships = "um"
|
||||||
|
pathGroupMemberships = "gm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type consulStore struct {
|
||||||
|
cli *consul.Client
|
||||||
|
kv *consul.KV
|
||||||
|
txn *consul.Txn
|
||||||
|
prefix string
|
||||||
|
logger *zap.SugaredLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConsulStore(logger *zap.Logger) (WritableStore, error) {
|
||||||
|
cfg := api.DefaultConfigWithLogger(wrapper.Wrap(logger))
|
||||||
|
cli, err := consul.NewClient(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &consulStore{
|
||||||
|
cli: cli,
|
||||||
|
kv: cli.KV(),
|
||||||
|
txn: cli.Txn(),
|
||||||
|
prefix: "authentricity/",
|
||||||
|
logger: logger.Sugar(),
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func ctxq(ctx context.Context) *consul.QueryOptions {
|
||||||
|
var qo *consul.QueryOptions
|
||||||
|
qo = qo.WithContext(ctx)
|
||||||
|
qo.RequireConsistent = MustRefetch(ctx)
|
||||||
|
return qo
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryMetadataToEntryMetadata(qm *consul.QueryMeta) EntryMetadata {
|
||||||
|
return EntryMetadata{
|
||||||
|
LastFetched: time.Now().Add(-qm.LastContact),
|
||||||
|
Revision: qm.LastIndex,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *consulStore) path(bits ...string) string {
|
||||||
|
return s.prefix + strings.Join(bits, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *consulStore) entityPath(id uuid.UUID) string {
|
||||||
|
return s.path(pathEntities, id.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *consulStore) namePath(name string) string {
|
||||||
|
return s.path(pathNames, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *consulStore) emailPath(email string) string {
|
||||||
|
return s.path(pathEmails, email)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *consulStore) userGroupPrefix(userID uuid.UUID) string {
|
||||||
|
return s.path(pathUserMemberships, userID.String(), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *consulStore) userGroupPath(userID, groupID uuid.UUID) string {
|
||||||
|
return s.path(pathUserMemberships, userID.String(), groupID.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *consulStore) groupUserPrefix(groupID uuid.UUID) string {
|
||||||
|
return s.path(pathGroupMemberships, groupID.String(), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *consulStore) groupUserPath(groupID, userID uuid.UUID) string {
|
||||||
|
return s.path(pathGroupMemberships, groupID.String(), userID.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *consulStore) GetEntity(ctx context.Context, id uuid.UUID) (
|
||||||
|
ent models.Entity,
|
||||||
|
md EntryMetadata,
|
||||||
|
err error,
|
||||||
|
) {
|
||||||
|
s.logger.Debugw("Lookup entity by ID", "id", id)
|
||||||
|
|
||||||
|
kv, qm, err := s.kv.Get(s.entityPath(id), ctxq(ctx))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
md = queryMetadataToEntryMetadata(qm)
|
||||||
|
if kv == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ent, err = models.ParseEntityJSON(kv.Value)
|
||||||
|
if ent != nil {
|
||||||
|
ent.SetID(id)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *consulStore) GetEntityIDByName(ctx context.Context, name string) (
|
||||||
|
id uuid.UUID,
|
||||||
|
md EntryMetadata,
|
||||||
|
err error,
|
||||||
|
) {
|
||||||
|
s.logger.Debugw("Lookup entity by name", "name", name)
|
||||||
|
|
||||||
|
kv, qm, err := s.kv.Get(s.namePath(name), ctxq(ctx))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
md = queryMetadataToEntryMetadata(qm)
|
||||||
|
if kv == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err = uuid.ParseBytes(kv.Value)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Debug("Looked up entity", "name", name, "id", id)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *consulStore) GetEntityIDByEmail(ctx context.Context, email string) (
|
||||||
|
id uuid.UUID,
|
||||||
|
md EntryMetadata,
|
||||||
|
err error,
|
||||||
|
) {
|
||||||
|
s.logger.Debugw("Lookup entity by email", "email", email)
|
||||||
|
|
||||||
|
kv, qm, err := s.kv.Get(s.emailPath(email), ctxq(ctx))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
md = queryMetadataToEntryMetadata(qm)
|
||||||
|
if kv == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err = uuid.ParseBytes(kv.Value)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Debugw("Looked up entity", "email", email, "id", id)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *consulStore) GetUserGroups(ctx context.Context, userID uuid.UUID) (
|
||||||
|
ids []uuid.UUID,
|
||||||
|
md EntryMetadata,
|
||||||
|
err error,
|
||||||
|
) {
|
||||||
|
s.logger.Debugw("Lookup user groups", "id", userID)
|
||||||
|
|
||||||
|
prefix := s.userGroupPrefix(userID)
|
||||||
|
keys, qm, err := s.kv.Keys(prefix, "", ctxq(ctx))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
md = queryMetadataToEntryMetadata(qm)
|
||||||
|
ids = make([]uuid.UUID, 0, len(keys))
|
||||||
|
for _, key := range keys {
|
||||||
|
var id uuid.UUID
|
||||||
|
id, err = uuid.Parse(strings.TrimPrefix(key, prefix))
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Interpreting group key '%s' (as '%s'): %w", key, strings.TrimPrefix(key, prefix), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *consulStore) GetGroupUsers(ctx context.Context, groupID uuid.UUID) (
|
||||||
|
ids []uuid.UUID,
|
||||||
|
md EntryMetadata,
|
||||||
|
err error,
|
||||||
|
) {
|
||||||
|
s.logger.Debugw("Lookup group users", "id", groupID)
|
||||||
|
|
||||||
|
prefix := s.groupUserPrefix(groupID)
|
||||||
|
keys, qm, err := s.kv.Keys(prefix, "", ctxq(ctx))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
md = queryMetadataToEntryMetadata(qm)
|
||||||
|
ids = make([]uuid.UUID, 0, len(keys))
|
||||||
|
for _, key := range keys {
|
||||||
|
var id uuid.UUID
|
||||||
|
id, err = uuid.Parse(strings.TrimPrefix(key, prefix))
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Interpreting user key '%s' (as '%s'): %w", key, strings.TrimPrefix(key, prefix), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Composite operations
|
||||||
|
func (s *consulStore) GetEntityByName(ctx context.Context, name string) (
|
||||||
|
ent models.Entity,
|
||||||
|
md EntryMetadata,
|
||||||
|
err error,
|
||||||
|
) {
|
||||||
|
id, md, err := s.GetEntityIDByName(ctx, name)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.GetEntity(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *consulStore) GetEntityByEmail(ctx context.Context, name string) (
|
||||||
|
ent models.Entity,
|
||||||
|
md EntryMetadata,
|
||||||
|
err error,
|
||||||
|
) {
|
||||||
|
id, md, err := s.GetEntityIDByEmail(ctx, name)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.GetEntity(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write Operations
|
||||||
|
func (s *consulStore) CreateEntity(ctx context.Context, ent models.Entity) (EntryMetadata, error) {
|
||||||
|
id := ent.ID()
|
||||||
|
data, err := json.Marshal(ent)
|
||||||
|
if err != nil {
|
||||||
|
return EntryMetadata{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ops := consul.TxnOps{{
|
||||||
|
KV: &consul.KVTxnOp{
|
||||||
|
Verb: consul.KVCAS,
|
||||||
|
Key: s.entityPath(id),
|
||||||
|
Value: data,
|
||||||
|
Index: 0,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
|
||||||
|
if name := ent.Name(); name != "" {
|
||||||
|
ops = append(ops, &consul.TxnOp{
|
||||||
|
KV: &consul.KVTxnOp{
|
||||||
|
Verb: consul.KVCAS,
|
||||||
|
Key: s.namePath(name),
|
||||||
|
Value: []byte(id.String()),
|
||||||
|
Index: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if email := ent.Email(); email != "" {
|
||||||
|
ops = append(ops, &consul.TxnOp{
|
||||||
|
KV: &consul.KVTxnOp{
|
||||||
|
Verb: consul.KVCAS,
|
||||||
|
Key: s.emailPath(email),
|
||||||
|
Value: []byte(id.String()),
|
||||||
|
Index: 0,
|
||||||
|
}})
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, _, qm, err := s.txn.Txn(ops, nil)
|
||||||
|
if err != nil {
|
||||||
|
return EntryMetadata{}, err
|
||||||
|
} else if !ok {
|
||||||
|
return queryMetadataToEntryMetadata(qm), ErrConflict
|
||||||
|
}
|
||||||
|
|
||||||
|
return queryMetadataToEntryMetadata(qm), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *consulStore) UpdateEntitySimple(ctx context.Context, ent models.Entity, oldmd EntryMetadata) error {
|
||||||
|
id := ent.ID()
|
||||||
|
data, err := json.Marshal(ent)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
p := &consul.KVPair{
|
||||||
|
Key: s.entityPath(id),
|
||||||
|
ModifyIndex: oldmd.Revision,
|
||||||
|
Value: data,
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, _, err := s.kv.CAS(p, new(consul.WriteOptions).WithContext(ctx))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if !ok {
|
||||||
|
return ErrConflict
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *consulStore) AddUserToGroup(ctx context.Context, userID, groupID uuid.UUID) error {
|
||||||
|
ops := consul.TxnOps{{
|
||||||
|
KV: &consul.KVTxnOp{
|
||||||
|
Verb: consul.KVSet,
|
||||||
|
Key: s.userGroupPath(userID, groupID),
|
||||||
|
Value: []byte{},
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
KV: &consul.KVTxnOp{
|
||||||
|
Verb: consul.KVSet,
|
||||||
|
Key: s.groupUserPath(groupID, userID),
|
||||||
|
Value: []byte{},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
ok, _, _, err := s.txn.Txn(ops, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if !ok {
|
||||||
|
return ErrConflict
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *consulStore) RemoveUserFromGroup(ctx context.Context, userID, groupID uuid.UUID) error {
|
||||||
|
ops := consul.TxnOps{{
|
||||||
|
KV: &consul.KVTxnOp{
|
||||||
|
Verb: consul.KVDelete,
|
||||||
|
Key: s.userGroupPath(userID, groupID),
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
KV: &consul.KVTxnOp{
|
||||||
|
Verb: consul.KVDelete,
|
||||||
|
Key: s.groupUserPath(groupID, userID),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
ok, _, _, err := s.txn.Txn(ops, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if !ok {
|
||||||
|
return ErrConflict
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
13
internal/store/errors.go
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package store
|
||||||
|
|
||||||
|
type constErr string
|
||||||
|
|
||||||
|
func (e constErr) Error() string {
|
||||||
|
return string(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
ErrConflict constErr = "record conflict"
|
||||||
|
ErrNotUser constErr = "entity was expected to be a user but is not"
|
||||||
|
ErrNotGroup constErr = "entity was expected to be a group but is not"
|
||||||
|
)
|
93
internal/store/store.go
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"go.e43.eu/authentricity/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EntryMetadata struct {
|
||||||
|
LastFetched time.Time
|
||||||
|
Revision uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
type Store interface {
|
||||||
|
GetEntity(ctx context.Context, id uuid.UUID) (models.Entity, EntryMetadata, error)
|
||||||
|
GetEntityIDByName(ctx context.Context, name string) (uuid.UUID, EntryMetadata, error)
|
||||||
|
GetEntityByName(ctx context.Context, name string) (models.Entity, EntryMetadata, error)
|
||||||
|
GetEntityIDByEmail(ctx context.Context, name string) (uuid.UUID, EntryMetadata, error)
|
||||||
|
GetEntityByEmail(ctx context.Context, name string) (models.Entity, EntryMetadata, error)
|
||||||
|
GetUserGroups(ctx context.Context, userID uuid.UUID) ([]uuid.UUID, EntryMetadata, error)
|
||||||
|
GetGroupUsers(ctx context.Context, groupID uuid.UUID) ([]uuid.UUID, EntryMetadata, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type WritableStore interface {
|
||||||
|
Store
|
||||||
|
CreateEntity(ctx context.Context, ent models.Entity) (EntryMetadata, error)
|
||||||
|
UpdateEntitySimple(ctx context.Context, ent models.Entity, oldmd EntryMetadata) error
|
||||||
|
AddUserToGroup(ctx context.Context, userID, groupID uuid.UUID) error
|
||||||
|
RemoveUserFromGroup(ctx context.Context, userID, groupID uuid.UUID) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type mustRefetchCtxKey struct{}
|
||||||
|
|
||||||
|
func WithMustRefetch(ctx context.Context) context.Context {
|
||||||
|
return context.WithValue(ctx, mustRefetchCtxKey{}, mustRefetchCtxKey{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustRefetch(ctx context.Context) bool {
|
||||||
|
return ctx.Value(mustRefetchCtxKey{}) != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetEntityIDByAnyName(ctx context.Context, st Store, name string) (id uuid.UUID, err error) {
|
||||||
|
cls, err := models.ClassifyName(name)
|
||||||
|
if err != nil {
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch cls {
|
||||||
|
case models.NameClassUUID:
|
||||||
|
id, err = uuid.Parse(name)
|
||||||
|
case models.NameClassUsername:
|
||||||
|
id, _, err = st.GetEntityIDByName(ctx, name)
|
||||||
|
case models.NameClassEmail:
|
||||||
|
id, _, err = st.GetEntityIDByEmail(ctx, name)
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("Unknown ID class %v", cls)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetEntityByAnyName(ctx context.Context, st Store, name string) (models.Entity, EntryMetadata, error) {
|
||||||
|
id, err := GetEntityIDByAnyName(ctx, st, name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, EntryMetadata{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return st.GetEntity(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUser(ctx context.Context, st Store, id uuid.UUID) (*models.UserRecord, EntryMetadata, error) {
|
||||||
|
ent, md, err := st.GetEntity(ctx, id)
|
||||||
|
if err != nil || ent == nil {
|
||||||
|
return nil, md, err
|
||||||
|
} else if ent.Type() != models.TypeUser {
|
||||||
|
return nil, md, ErrNotUser
|
||||||
|
}
|
||||||
|
|
||||||
|
return ent.(*models.UserRecord), md, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetGroup(ctx context.Context, st Store, id uuid.UUID) (*models.GroupRecord, EntryMetadata, error) {
|
||||||
|
ent, md, err := st.GetEntity(ctx, id)
|
||||||
|
if err != nil || ent == nil {
|
||||||
|
return nil, md, err
|
||||||
|
} else if ent.Type() != models.TypeGroup {
|
||||||
|
return nil, md, ErrNotGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
return ent.(*models.GroupRecord), md, err
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
../../../varlink/eu.e43.authentricity.HostAgent.varlink
|
|
@ -0,0 +1,771 @@
|
||||||
|
// Code generated by github.com/varlink/go/cmd/varlink-go-interface-generator, DO NOT EDIT.
|
||||||
|
|
||||||
|
// Interface for Authentricity-specific host agent requests
|
||||||
|
// # The hostagent also implements the io.systemd.UserDatabase interface
|
||||||
|
package eue43authentricityhostagent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/varlink/go/varlink"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Generated type declarations
|
||||||
|
|
||||||
|
// A generic error - rquivalent to HTTP 500 Internal Server Error
|
||||||
|
type ServiceNotAvailable struct {
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ServiceNotAvailable) Error() string {
|
||||||
|
s := "eu.e43.authentricity.HostAgent.ServiceNotAvailable"
|
||||||
|
s += fmt.Sprintf("(Description: %v)", e.Description)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
type NoRecordFound struct{}
|
||||||
|
|
||||||
|
func (e NoRecordFound) Error() string {
|
||||||
|
s := "eu.e43.authentricity.HostAgent.NoRecordFound"
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
type MoreRequired struct{}
|
||||||
|
|
||||||
|
func (e MoreRequired) Error() string {
|
||||||
|
s := "eu.e43.authentricity.HostAgent.MoreRequired"
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConflictingRecordFound struct{}
|
||||||
|
|
||||||
|
func (e ConflictingRecordFound) Error() string {
|
||||||
|
s := "eu.e43.authentricity.HostAgent.ConflictingRecordFound"
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func Dispatch_Error(err error) error {
|
||||||
|
if e, ok := err.(*varlink.Error); ok {
|
||||||
|
switch e.Name {
|
||||||
|
case "eu.e43.authentricity.HostAgent.ServiceNotAvailable":
|
||||||
|
errorRawParameters := e.Parameters.(*json.RawMessage)
|
||||||
|
if errorRawParameters == nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
var param ServiceNotAvailable
|
||||||
|
err := json.Unmarshal(*errorRawParameters, ¶m)
|
||||||
|
if err != nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return ¶m
|
||||||
|
case "eu.e43.authentricity.HostAgent.NoRecordFound":
|
||||||
|
errorRawParameters := e.Parameters.(*json.RawMessage)
|
||||||
|
if errorRawParameters == nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
var param NoRecordFound
|
||||||
|
err := json.Unmarshal(*errorRawParameters, ¶m)
|
||||||
|
if err != nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return ¶m
|
||||||
|
case "eu.e43.authentricity.HostAgent.MoreRequired":
|
||||||
|
errorRawParameters := e.Parameters.(*json.RawMessage)
|
||||||
|
if errorRawParameters == nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
var param MoreRequired
|
||||||
|
err := json.Unmarshal(*errorRawParameters, ¶m)
|
||||||
|
if err != nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return ¶m
|
||||||
|
case "eu.e43.authentricity.HostAgent.ConflictingRecordFound":
|
||||||
|
errorRawParameters := e.Parameters.(*json.RawMessage)
|
||||||
|
if errorRawParameters == nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
var param ConflictingRecordFound
|
||||||
|
err := json.Unmarshal(*errorRawParameters, ¶m)
|
||||||
|
if err != nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return ¶m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generated client method calls
|
||||||
|
|
||||||
|
// Lookup entity by Unix ID, name, e-mail address or UUID
|
||||||
|
// The type can be identified by looking at the "@type" member
|
||||||
|
type GetEntity_methods struct{}
|
||||||
|
|
||||||
|
func GetEntity() GetEntity_methods { return GetEntity_methods{} }
|
||||||
|
|
||||||
|
func (m GetEntity_methods) Call(ctx context.Context, c *varlink.Connection, unixId_in_ *int64, name_in_ *string, email_in_ *string, uuid_in_ *string) (record_out_ json.RawMessage, err_ error) {
|
||||||
|
receive, err_ := m.Send(ctx, c, 0, unixId_in_, name_in_, email_in_, uuid_in_)
|
||||||
|
if err_ != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
record_out_, _, err_ = receive(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m GetEntity_methods) Send(ctx context.Context, c *varlink.Connection, flags uint64, unixId_in_ *int64, name_in_ *string, email_in_ *string, uuid_in_ *string) (func(ctx context.Context) (json.RawMessage, uint64, error), error) {
|
||||||
|
var in struct {
|
||||||
|
UnixId *int64 `json:"unixId,omitempty"`
|
||||||
|
Name *string `json:"name,omitempty"`
|
||||||
|
Email *string `json:"email,omitempty"`
|
||||||
|
Uuid *string `json:"uuid,omitempty"`
|
||||||
|
}
|
||||||
|
in.UnixId = unixId_in_
|
||||||
|
in.Name = name_in_
|
||||||
|
in.Email = email_in_
|
||||||
|
in.Uuid = uuid_in_
|
||||||
|
receive, err := c.Send(ctx, "eu.e43.authentricity.HostAgent.GetEntity", in, flags)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return func(context.Context) (record_out_ json.RawMessage, flags uint64, err error) {
|
||||||
|
var out struct {
|
||||||
|
Record json.RawMessage `json:"record"`
|
||||||
|
}
|
||||||
|
flags, err = receive(ctx, &out)
|
||||||
|
if err != nil {
|
||||||
|
err = Dispatch_Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
record_out_ = out.Record
|
||||||
|
return
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m GetEntity_methods) Upgrade(ctx context.Context, c *varlink.Connection, unixId_in_ *int64, name_in_ *string, email_in_ *string, uuid_in_ *string) (func(ctx context.Context) (record_out_ json.RawMessage, flags uint64, conn varlink.ReadWriterContext, err_ error), error) {
|
||||||
|
var in struct {
|
||||||
|
UnixId *int64 `json:"unixId,omitempty"`
|
||||||
|
Name *string `json:"name,omitempty"`
|
||||||
|
Email *string `json:"email,omitempty"`
|
||||||
|
Uuid *string `json:"uuid,omitempty"`
|
||||||
|
}
|
||||||
|
in.UnixId = unixId_in_
|
||||||
|
in.Name = name_in_
|
||||||
|
in.Email = email_in_
|
||||||
|
in.Uuid = uuid_in_
|
||||||
|
receive, err := c.Upgrade(ctx, "eu.e43.authentricity.HostAgent.GetEntity", in)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return func(context.Context) (record_out_ json.RawMessage, flags uint64, conn varlink.ReadWriterContext, err error) {
|
||||||
|
var out struct {
|
||||||
|
Record json.RawMessage `json:"record"`
|
||||||
|
}
|
||||||
|
flags, conn, err = receive(ctx, &out)
|
||||||
|
if err != nil {
|
||||||
|
err = Dispatch_Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
record_out_ = out.Record
|
||||||
|
return
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Based upon a Unix ID, name or e-mail address, lookup the corresponding
|
||||||
|
// entity UUID
|
||||||
|
type GetEntityID_methods struct{}
|
||||||
|
|
||||||
|
func GetEntityID() GetEntityID_methods { return GetEntityID_methods{} }
|
||||||
|
|
||||||
|
func (m GetEntityID_methods) Call(ctx context.Context, c *varlink.Connection, unixId_in_ *int64, name_in_ *string, email_in_ *string) (uuid_out_ string, err_ error) {
|
||||||
|
receive, err_ := m.Send(ctx, c, 0, unixId_in_, name_in_, email_in_)
|
||||||
|
if err_ != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uuid_out_, _, err_ = receive(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m GetEntityID_methods) Send(ctx context.Context, c *varlink.Connection, flags uint64, unixId_in_ *int64, name_in_ *string, email_in_ *string) (func(ctx context.Context) (string, uint64, error), error) {
|
||||||
|
var in struct {
|
||||||
|
UnixId *int64 `json:"unixId,omitempty"`
|
||||||
|
Name *string `json:"name,omitempty"`
|
||||||
|
Email *string `json:"email,omitempty"`
|
||||||
|
}
|
||||||
|
in.UnixId = unixId_in_
|
||||||
|
in.Name = name_in_
|
||||||
|
in.Email = email_in_
|
||||||
|
receive, err := c.Send(ctx, "eu.e43.authentricity.HostAgent.GetEntityID", in, flags)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return func(context.Context) (uuid_out_ string, flags uint64, err error) {
|
||||||
|
var out struct {
|
||||||
|
Uuid string `json:"uuid"`
|
||||||
|
}
|
||||||
|
flags, err = receive(ctx, &out)
|
||||||
|
if err != nil {
|
||||||
|
err = Dispatch_Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uuid_out_ = out.Uuid
|
||||||
|
return
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m GetEntityID_methods) Upgrade(ctx context.Context, c *varlink.Connection, unixId_in_ *int64, name_in_ *string, email_in_ *string) (func(ctx context.Context) (uuid_out_ string, flags uint64, conn varlink.ReadWriterContext, err_ error), error) {
|
||||||
|
var in struct {
|
||||||
|
UnixId *int64 `json:"unixId,omitempty"`
|
||||||
|
Name *string `json:"name,omitempty"`
|
||||||
|
Email *string `json:"email,omitempty"`
|
||||||
|
}
|
||||||
|
in.UnixId = unixId_in_
|
||||||
|
in.Name = name_in_
|
||||||
|
in.Email = email_in_
|
||||||
|
receive, err := c.Upgrade(ctx, "eu.e43.authentricity.HostAgent.GetEntityID", in)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return func(context.Context) (uuid_out_ string, flags uint64, conn varlink.ReadWriterContext, err error) {
|
||||||
|
var out struct {
|
||||||
|
Uuid string `json:"uuid"`
|
||||||
|
}
|
||||||
|
flags, conn, err = receive(ctx, &out)
|
||||||
|
if err != nil {
|
||||||
|
err = Dispatch_Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uuid_out_ = out.Uuid
|
||||||
|
return
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Based upon a UUID, lookup the corresponding Unix ID
|
||||||
|
// If unixID is absent, enumerates all bindings
|
||||||
|
type GetUnixID_methods struct{}
|
||||||
|
|
||||||
|
func GetUnixID() GetUnixID_methods { return GetUnixID_methods{} }
|
||||||
|
|
||||||
|
func (m GetUnixID_methods) Call(ctx context.Context, c *varlink.Connection, uuid_in_ *string) (uuid_out_ string, unixId_out_ int64, err_ error) {
|
||||||
|
receive, err_ := m.Send(ctx, c, 0, uuid_in_)
|
||||||
|
if err_ != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uuid_out_, unixId_out_, _, err_ = receive(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m GetUnixID_methods) Send(ctx context.Context, c *varlink.Connection, flags uint64, uuid_in_ *string) (func(ctx context.Context) (string, int64, uint64, error), error) {
|
||||||
|
var in struct {
|
||||||
|
Uuid *string `json:"uuid,omitempty"`
|
||||||
|
}
|
||||||
|
in.Uuid = uuid_in_
|
||||||
|
receive, err := c.Send(ctx, "eu.e43.authentricity.HostAgent.GetUnixID", in, flags)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return func(context.Context) (uuid_out_ string, unixId_out_ int64, flags uint64, err error) {
|
||||||
|
var out struct {
|
||||||
|
Uuid string `json:"uuid"`
|
||||||
|
UnixId int64 `json:"unixId"`
|
||||||
|
}
|
||||||
|
flags, err = receive(ctx, &out)
|
||||||
|
if err != nil {
|
||||||
|
err = Dispatch_Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uuid_out_ = out.Uuid
|
||||||
|
unixId_out_ = out.UnixId
|
||||||
|
return
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m GetUnixID_methods) Upgrade(ctx context.Context, c *varlink.Connection, uuid_in_ *string) (func(ctx context.Context) (uuid_out_ string, unixId_out_ int64, flags uint64, conn varlink.ReadWriterContext, err_ error), error) {
|
||||||
|
var in struct {
|
||||||
|
Uuid *string `json:"uuid,omitempty"`
|
||||||
|
}
|
||||||
|
in.Uuid = uuid_in_
|
||||||
|
receive, err := c.Upgrade(ctx, "eu.e43.authentricity.HostAgent.GetUnixID", in)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return func(context.Context) (uuid_out_ string, unixId_out_ int64, flags uint64, conn varlink.ReadWriterContext, err error) {
|
||||||
|
var out struct {
|
||||||
|
Uuid string `json:"uuid"`
|
||||||
|
UnixId int64 `json:"unixId"`
|
||||||
|
}
|
||||||
|
flags, conn, err = receive(ctx, &out)
|
||||||
|
if err != nil {
|
||||||
|
err = Dispatch_Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uuid_out_ = out.Uuid
|
||||||
|
unixId_out_ = out.UnixId
|
||||||
|
return
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup the IDs of all groups that a user belongs to
|
||||||
|
type GetUserGroups_methods struct{}
|
||||||
|
|
||||||
|
func GetUserGroups() GetUserGroups_methods { return GetUserGroups_methods{} }
|
||||||
|
|
||||||
|
func (m GetUserGroups_methods) Call(ctx context.Context, c *varlink.Connection, userUuid_in_ string) (groupUuid_out_ string, err_ error) {
|
||||||
|
receive, err_ := m.Send(ctx, c, 0, userUuid_in_)
|
||||||
|
if err_ != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
groupUuid_out_, _, err_ = receive(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m GetUserGroups_methods) Send(ctx context.Context, c *varlink.Connection, flags uint64, userUuid_in_ string) (func(ctx context.Context) (string, uint64, error), error) {
|
||||||
|
var in struct {
|
||||||
|
UserUuid string `json:"userUuid"`
|
||||||
|
}
|
||||||
|
in.UserUuid = userUuid_in_
|
||||||
|
receive, err := c.Send(ctx, "eu.e43.authentricity.HostAgent.GetUserGroups", in, flags)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return func(context.Context) (groupUuid_out_ string, flags uint64, err error) {
|
||||||
|
var out struct {
|
||||||
|
GroupUuid string `json:"groupUuid"`
|
||||||
|
}
|
||||||
|
flags, err = receive(ctx, &out)
|
||||||
|
if err != nil {
|
||||||
|
err = Dispatch_Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
groupUuid_out_ = out.GroupUuid
|
||||||
|
return
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m GetUserGroups_methods) Upgrade(ctx context.Context, c *varlink.Connection, userUuid_in_ string) (func(ctx context.Context) (groupUuid_out_ string, flags uint64, conn varlink.ReadWriterContext, err_ error), error) {
|
||||||
|
var in struct {
|
||||||
|
UserUuid string `json:"userUuid"`
|
||||||
|
}
|
||||||
|
in.UserUuid = userUuid_in_
|
||||||
|
receive, err := c.Upgrade(ctx, "eu.e43.authentricity.HostAgent.GetUserGroups", in)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return func(context.Context) (groupUuid_out_ string, flags uint64, conn varlink.ReadWriterContext, err error) {
|
||||||
|
var out struct {
|
||||||
|
GroupUuid string `json:"groupUuid"`
|
||||||
|
}
|
||||||
|
flags, conn, err = receive(ctx, &out)
|
||||||
|
if err != nil {
|
||||||
|
err = Dispatch_Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
groupUuid_out_ = out.GroupUuid
|
||||||
|
return
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup the IDs of all users that are in a group
|
||||||
|
type GetGroupUsers_methods struct{}
|
||||||
|
|
||||||
|
func GetGroupUsers() GetGroupUsers_methods { return GetGroupUsers_methods{} }
|
||||||
|
|
||||||
|
func (m GetGroupUsers_methods) Call(ctx context.Context, c *varlink.Connection, groupUuid_in_ string) (userUuid_out_ string, err_ error) {
|
||||||
|
receive, err_ := m.Send(ctx, c, 0, groupUuid_in_)
|
||||||
|
if err_ != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userUuid_out_, _, err_ = receive(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m GetGroupUsers_methods) Send(ctx context.Context, c *varlink.Connection, flags uint64, groupUuid_in_ string) (func(ctx context.Context) (string, uint64, error), error) {
|
||||||
|
var in struct {
|
||||||
|
GroupUuid string `json:"groupUuid"`
|
||||||
|
}
|
||||||
|
in.GroupUuid = groupUuid_in_
|
||||||
|
receive, err := c.Send(ctx, "eu.e43.authentricity.HostAgent.GetGroupUsers", in, flags)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return func(context.Context) (userUuid_out_ string, flags uint64, err error) {
|
||||||
|
var out struct {
|
||||||
|
UserUuid string `json:"userUuid"`
|
||||||
|
}
|
||||||
|
flags, err = receive(ctx, &out)
|
||||||
|
if err != nil {
|
||||||
|
err = Dispatch_Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userUuid_out_ = out.UserUuid
|
||||||
|
return
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m GetGroupUsers_methods) Upgrade(ctx context.Context, c *varlink.Connection, groupUuid_in_ string) (func(ctx context.Context) (userUuid_out_ string, flags uint64, conn varlink.ReadWriterContext, err_ error), error) {
|
||||||
|
var in struct {
|
||||||
|
GroupUuid string `json:"groupUuid"`
|
||||||
|
}
|
||||||
|
in.GroupUuid = groupUuid_in_
|
||||||
|
receive, err := c.Upgrade(ctx, "eu.e43.authentricity.HostAgent.GetGroupUsers", in)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return func(context.Context) (userUuid_out_ string, flags uint64, conn varlink.ReadWriterContext, err error) {
|
||||||
|
var out struct {
|
||||||
|
UserUuid string `json:"userUuid"`
|
||||||
|
}
|
||||||
|
flags, conn, err = receive(ctx, &out)
|
||||||
|
if err != nil {
|
||||||
|
err = Dispatch_Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userUuid_out_ = out.UserUuid
|
||||||
|
return
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-fetch the information for a specific entity from the source
|
||||||
|
type Refetch_methods struct{}
|
||||||
|
|
||||||
|
func Refetch() Refetch_methods { return Refetch_methods{} }
|
||||||
|
|
||||||
|
func (m Refetch_methods) Call(ctx context.Context, c *varlink.Connection, uuid_in_ string) (record_out_ json.RawMessage, err_ error) {
|
||||||
|
receive, err_ := m.Send(ctx, c, 0, uuid_in_)
|
||||||
|
if err_ != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
record_out_, _, err_ = receive(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Refetch_methods) Send(ctx context.Context, c *varlink.Connection, flags uint64, uuid_in_ string) (func(ctx context.Context) (json.RawMessage, uint64, error), error) {
|
||||||
|
var in struct {
|
||||||
|
Uuid string `json:"uuid"`
|
||||||
|
}
|
||||||
|
in.Uuid = uuid_in_
|
||||||
|
receive, err := c.Send(ctx, "eu.e43.authentricity.HostAgent.Refetch", in, flags)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return func(context.Context) (record_out_ json.RawMessage, flags uint64, err error) {
|
||||||
|
var out struct {
|
||||||
|
Record json.RawMessage `json:"record"`
|
||||||
|
}
|
||||||
|
flags, err = receive(ctx, &out)
|
||||||
|
if err != nil {
|
||||||
|
err = Dispatch_Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
record_out_ = out.Record
|
||||||
|
return
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Refetch_methods) Upgrade(ctx context.Context, c *varlink.Connection, uuid_in_ string) (func(ctx context.Context) (record_out_ json.RawMessage, flags uint64, conn varlink.ReadWriterContext, err_ error), error) {
|
||||||
|
var in struct {
|
||||||
|
Uuid string `json:"uuid"`
|
||||||
|
}
|
||||||
|
in.Uuid = uuid_in_
|
||||||
|
receive, err := c.Upgrade(ctx, "eu.e43.authentricity.HostAgent.Refetch", in)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return func(context.Context) (record_out_ json.RawMessage, flags uint64, conn varlink.ReadWriterContext, err error) {
|
||||||
|
var out struct {
|
||||||
|
Record json.RawMessage `json:"record"`
|
||||||
|
}
|
||||||
|
flags, conn, err = receive(ctx, &out)
|
||||||
|
if err != nil {
|
||||||
|
err = Dispatch_Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
record_out_ = out.Record
|
||||||
|
return
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generated service interface with all methods
|
||||||
|
|
||||||
|
type eue43authentricityhostagentInterface interface {
|
||||||
|
GetEntity(ctx context.Context, c VarlinkCall, unixId_ *int64, name_ *string, email_ *string, uuid_ *string) error
|
||||||
|
GetEntityID(ctx context.Context, c VarlinkCall, unixId_ *int64, name_ *string, email_ *string) error
|
||||||
|
GetUnixID(ctx context.Context, c VarlinkCall, uuid_ *string) error
|
||||||
|
GetUserGroups(ctx context.Context, c VarlinkCall, userUuid_ string) error
|
||||||
|
GetGroupUsers(ctx context.Context, c VarlinkCall, groupUuid_ string) error
|
||||||
|
Refetch(ctx context.Context, c VarlinkCall, uuid_ string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generated service object with all methods
|
||||||
|
|
||||||
|
type VarlinkCall struct{ varlink.Call }
|
||||||
|
|
||||||
|
// Generated reply methods for all varlink errors
|
||||||
|
|
||||||
|
// A generic error - rquivalent to HTTP 500 Internal Server Error
|
||||||
|
func (c *VarlinkCall) ReplyServiceNotAvailable(ctx context.Context, description_ string) error {
|
||||||
|
var out ServiceNotAvailable
|
||||||
|
out.Description = description_
|
||||||
|
return c.ReplyError(ctx, "eu.e43.authentricity.HostAgent.ServiceNotAvailable", &out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *VarlinkCall) ReplyNoRecordFound(ctx context.Context) error {
|
||||||
|
var out NoRecordFound
|
||||||
|
return c.ReplyError(ctx, "eu.e43.authentricity.HostAgent.NoRecordFound", &out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *VarlinkCall) ReplyMoreRequired(ctx context.Context) error {
|
||||||
|
var out MoreRequired
|
||||||
|
return c.ReplyError(ctx, "eu.e43.authentricity.HostAgent.MoreRequired", &out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *VarlinkCall) ReplyConflictingRecordFound(ctx context.Context) error {
|
||||||
|
var out ConflictingRecordFound
|
||||||
|
return c.ReplyError(ctx, "eu.e43.authentricity.HostAgent.ConflictingRecordFound", &out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generated reply methods for all varlink methods
|
||||||
|
|
||||||
|
func (c *VarlinkCall) ReplyGetEntity(ctx context.Context, record_ json.RawMessage) error {
|
||||||
|
var out struct {
|
||||||
|
Record json.RawMessage `json:"record"`
|
||||||
|
}
|
||||||
|
out.Record = record_
|
||||||
|
return c.Reply(ctx, &out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *VarlinkCall) ReplyGetEntityID(ctx context.Context, uuid_ string) error {
|
||||||
|
var out struct {
|
||||||
|
Uuid string `json:"uuid"`
|
||||||
|
}
|
||||||
|
out.Uuid = uuid_
|
||||||
|
return c.Reply(ctx, &out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *VarlinkCall) ReplyGetUnixID(ctx context.Context, uuid_ string, unixId_ int64) error {
|
||||||
|
var out struct {
|
||||||
|
Uuid string `json:"uuid"`
|
||||||
|
UnixId int64 `json:"unixId"`
|
||||||
|
}
|
||||||
|
out.Uuid = uuid_
|
||||||
|
out.UnixId = unixId_
|
||||||
|
return c.Reply(ctx, &out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *VarlinkCall) ReplyGetUserGroups(ctx context.Context, groupUuid_ string) error {
|
||||||
|
var out struct {
|
||||||
|
GroupUuid string `json:"groupUuid"`
|
||||||
|
}
|
||||||
|
out.GroupUuid = groupUuid_
|
||||||
|
return c.Reply(ctx, &out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *VarlinkCall) ReplyGetGroupUsers(ctx context.Context, userUuid_ string) error {
|
||||||
|
var out struct {
|
||||||
|
UserUuid string `json:"userUuid"`
|
||||||
|
}
|
||||||
|
out.UserUuid = userUuid_
|
||||||
|
return c.Reply(ctx, &out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *VarlinkCall) ReplyRefetch(ctx context.Context, record_ json.RawMessage) error {
|
||||||
|
var out struct {
|
||||||
|
Record json.RawMessage `json:"record"`
|
||||||
|
}
|
||||||
|
out.Record = record_
|
||||||
|
return c.Reply(ctx, &out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generated dummy implementations for all varlink methods
|
||||||
|
|
||||||
|
// Lookup entity by Unix ID, name, e-mail address or UUID
|
||||||
|
// The type can be identified by looking at the "@type" member
|
||||||
|
func (s *VarlinkInterface) GetEntity(ctx context.Context, c VarlinkCall, unixId_ *int64, name_ *string, email_ *string, uuid_ *string) error {
|
||||||
|
return c.ReplyMethodNotImplemented(ctx, "eu.e43.authentricity.HostAgent.GetEntity")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Based upon a Unix ID, name or e-mail address, lookup the corresponding
|
||||||
|
// entity UUID
|
||||||
|
func (s *VarlinkInterface) GetEntityID(ctx context.Context, c VarlinkCall, unixId_ *int64, name_ *string, email_ *string) error {
|
||||||
|
return c.ReplyMethodNotImplemented(ctx, "eu.e43.authentricity.HostAgent.GetEntityID")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Based upon a UUID, lookup the corresponding Unix ID
|
||||||
|
// If unixID is absent, enumerates all bindings
|
||||||
|
func (s *VarlinkInterface) GetUnixID(ctx context.Context, c VarlinkCall, uuid_ *string) error {
|
||||||
|
return c.ReplyMethodNotImplemented(ctx, "eu.e43.authentricity.HostAgent.GetUnixID")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup the IDs of all groups that a user belongs to
|
||||||
|
func (s *VarlinkInterface) GetUserGroups(ctx context.Context, c VarlinkCall, userUuid_ string) error {
|
||||||
|
return c.ReplyMethodNotImplemented(ctx, "eu.e43.authentricity.HostAgent.GetUserGroups")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup the IDs of all users that are in a group
|
||||||
|
func (s *VarlinkInterface) GetGroupUsers(ctx context.Context, c VarlinkCall, groupUuid_ string) error {
|
||||||
|
return c.ReplyMethodNotImplemented(ctx, "eu.e43.authentricity.HostAgent.GetGroupUsers")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-fetch the information for a specific entity from the source
|
||||||
|
func (s *VarlinkInterface) Refetch(ctx context.Context, c VarlinkCall, uuid_ string) error {
|
||||||
|
return c.ReplyMethodNotImplemented(ctx, "eu.e43.authentricity.HostAgent.Refetch")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generated method call dispatcher
|
||||||
|
|
||||||
|
func (s *VarlinkInterface) VarlinkDispatch(ctx context.Context, call varlink.Call, methodname string) error {
|
||||||
|
switch methodname {
|
||||||
|
case "GetEntity":
|
||||||
|
var in struct {
|
||||||
|
UnixId *int64 `json:"unixId,omitempty"`
|
||||||
|
Name *string `json:"name,omitempty"`
|
||||||
|
Email *string `json:"email,omitempty"`
|
||||||
|
Uuid *string `json:"uuid,omitempty"`
|
||||||
|
}
|
||||||
|
err := call.GetParameters(&in)
|
||||||
|
if err != nil {
|
||||||
|
return call.ReplyInvalidParameter(ctx, "parameters")
|
||||||
|
}
|
||||||
|
return s.eue43authentricityhostagentInterface.GetEntity(ctx, VarlinkCall{call}, in.UnixId, in.Name, in.Email, in.Uuid)
|
||||||
|
|
||||||
|
case "GetEntityID":
|
||||||
|
var in struct {
|
||||||
|
UnixId *int64 `json:"unixId,omitempty"`
|
||||||
|
Name *string `json:"name,omitempty"`
|
||||||
|
Email *string `json:"email,omitempty"`
|
||||||
|
}
|
||||||
|
err := call.GetParameters(&in)
|
||||||
|
if err != nil {
|
||||||
|
return call.ReplyInvalidParameter(ctx, "parameters")
|
||||||
|
}
|
||||||
|
return s.eue43authentricityhostagentInterface.GetEntityID(ctx, VarlinkCall{call}, in.UnixId, in.Name, in.Email)
|
||||||
|
|
||||||
|
case "GetUnixID":
|
||||||
|
var in struct {
|
||||||
|
Uuid *string `json:"uuid,omitempty"`
|
||||||
|
}
|
||||||
|
err := call.GetParameters(&in)
|
||||||
|
if err != nil {
|
||||||
|
return call.ReplyInvalidParameter(ctx, "parameters")
|
||||||
|
}
|
||||||
|
return s.eue43authentricityhostagentInterface.GetUnixID(ctx, VarlinkCall{call}, in.Uuid)
|
||||||
|
|
||||||
|
case "GetUserGroups":
|
||||||
|
var in struct {
|
||||||
|
UserUuid string `json:"userUuid"`
|
||||||
|
}
|
||||||
|
err := call.GetParameters(&in)
|
||||||
|
if err != nil {
|
||||||
|
return call.ReplyInvalidParameter(ctx, "parameters")
|
||||||
|
}
|
||||||
|
return s.eue43authentricityhostagentInterface.GetUserGroups(ctx, VarlinkCall{call}, in.UserUuid)
|
||||||
|
|
||||||
|
case "GetGroupUsers":
|
||||||
|
var in struct {
|
||||||
|
GroupUuid string `json:"groupUuid"`
|
||||||
|
}
|
||||||
|
err := call.GetParameters(&in)
|
||||||
|
if err != nil {
|
||||||
|
return call.ReplyInvalidParameter(ctx, "parameters")
|
||||||
|
}
|
||||||
|
return s.eue43authentricityhostagentInterface.GetGroupUsers(ctx, VarlinkCall{call}, in.GroupUuid)
|
||||||
|
|
||||||
|
case "Refetch":
|
||||||
|
var in struct {
|
||||||
|
Uuid string `json:"uuid"`
|
||||||
|
}
|
||||||
|
err := call.GetParameters(&in)
|
||||||
|
if err != nil {
|
||||||
|
return call.ReplyInvalidParameter(ctx, "parameters")
|
||||||
|
}
|
||||||
|
return s.eue43authentricityhostagentInterface.Refetch(ctx, VarlinkCall{call}, in.Uuid)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return call.ReplyMethodNotFound(ctx, methodname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generated varlink interface name
|
||||||
|
|
||||||
|
func (s *VarlinkInterface) VarlinkGetName() string {
|
||||||
|
return `eu.e43.authentricity.HostAgent`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generated varlink interface description
|
||||||
|
|
||||||
|
func (s *VarlinkInterface) VarlinkGetDescription() string {
|
||||||
|
return `# Interface for Authentricity-specific host agent requests
|
||||||
|
#
|
||||||
|
# The hostagent also implements the io.systemd.UserDatabase interface
|
||||||
|
interface eu.e43.authentricity.HostAgent
|
||||||
|
|
||||||
|
# Lookup entity by Unix ID, name, e-mail address or UUID
|
||||||
|
# The type can be identified by looking at the "@type" member
|
||||||
|
method GetEntity(
|
||||||
|
unixId: ?int,
|
||||||
|
name: ?string,
|
||||||
|
email: ?string,
|
||||||
|
uuid: ?string
|
||||||
|
) -> (
|
||||||
|
record: object
|
||||||
|
)
|
||||||
|
|
||||||
|
# Based upon a Unix ID, name or e-mail address, lookup the corresponding
|
||||||
|
# entity UUID
|
||||||
|
method GetEntityID(
|
||||||
|
unixId: ?int,
|
||||||
|
name: ?string,
|
||||||
|
email: ?string
|
||||||
|
) -> (
|
||||||
|
uuid: string
|
||||||
|
)
|
||||||
|
|
||||||
|
# Based upon a UUID, lookup the corresponding Unix ID
|
||||||
|
# If unixID is absent, enumerates all bindings
|
||||||
|
method GetUnixID(
|
||||||
|
uuid: ?string
|
||||||
|
) -> (
|
||||||
|
uuid: string,
|
||||||
|
unixId: int
|
||||||
|
)
|
||||||
|
|
||||||
|
# Lookup the IDs of all groups that a user belongs to
|
||||||
|
method GetUserGroups(
|
||||||
|
userUuid: string
|
||||||
|
) -> (
|
||||||
|
groupUuid: string
|
||||||
|
)
|
||||||
|
|
||||||
|
# Lookup the IDs of all users that are in a group
|
||||||
|
method GetGroupUsers(
|
||||||
|
groupUuid: string
|
||||||
|
) -> (
|
||||||
|
userUuid: string
|
||||||
|
)
|
||||||
|
|
||||||
|
# Re-fetch the information for a specific entity from the source
|
||||||
|
method Refetch(
|
||||||
|
uuid: string
|
||||||
|
) -> (
|
||||||
|
record: object
|
||||||
|
)
|
||||||
|
|
||||||
|
# A generic error - rquivalent to HTTP 500 Internal Server Error
|
||||||
|
error ServiceNotAvailable(description: string)
|
||||||
|
error NoRecordFound()
|
||||||
|
error MoreRequired()
|
||||||
|
error ConflictingRecordFound()
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generated service interface
|
||||||
|
|
||||||
|
type VarlinkInterface struct {
|
||||||
|
eue43authentricityhostagentInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func VarlinkNew(m eue43authentricityhostagentInterface) *VarlinkInterface {
|
||||||
|
return &VarlinkInterface{m}
|
||||||
|
}
|
3
internal/varlink/eue43authentricityhostagent/gen.go
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
package eue43authentricityhostagent
|
||||||
|
|
||||||
|
//go:generate go run github.com/varlink/go/cmd/varlink-go-interface-generator eu.e43.authentricity.HostAgent.varlink
|
3
internal/varlink/iosystemduserdatabase/gen.go
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
package iosystemduserdatabase
|
||||||
|
|
||||||
|
//go:generate go run github.com/varlink/go/cmd/varlink-go-interface-generator io.systemd.UserDatabase.varlink
|
|
@ -0,0 +1 @@
|
||||||
|
../../../varlink/io.systemd.UserDatabase.varlink
|
511
internal/varlink/iosystemduserdatabase/iosystemduserdatabase.go
Normal file
|
@ -0,0 +1,511 @@
|
||||||
|
// Code generated by github.com/varlink/go/cmd/varlink-go-interface-generator, DO NOT EDIT.
|
||||||
|
|
||||||
|
package iosystemduserdatabase
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/varlink/go/varlink"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Generated type declarations
|
||||||
|
|
||||||
|
type NoRecordFound struct{}
|
||||||
|
|
||||||
|
func (e NoRecordFound) Error() string {
|
||||||
|
s := "io.systemd.UserDatabase.NoRecordFound"
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
type BadService struct{}
|
||||||
|
|
||||||
|
func (e BadService) Error() string {
|
||||||
|
s := "io.systemd.UserDatabase.BadService"
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServiceNotAvailable struct{}
|
||||||
|
|
||||||
|
func (e ServiceNotAvailable) Error() string {
|
||||||
|
s := "io.systemd.UserDatabase.ServiceNotAvailable"
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConflictingRecordFound struct{}
|
||||||
|
|
||||||
|
func (e ConflictingRecordFound) Error() string {
|
||||||
|
s := "io.systemd.UserDatabase.ConflictingRecordFound"
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
type EnumerationNotSupported struct{}
|
||||||
|
|
||||||
|
func (e EnumerationNotSupported) Error() string {
|
||||||
|
s := "io.systemd.UserDatabase.EnumerationNotSupported"
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func Dispatch_Error(err error) error {
|
||||||
|
if e, ok := err.(*varlink.Error); ok {
|
||||||
|
switch e.Name {
|
||||||
|
case "io.systemd.UserDatabase.NoRecordFound":
|
||||||
|
errorRawParameters := e.Parameters.(*json.RawMessage)
|
||||||
|
if errorRawParameters == nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
var param NoRecordFound
|
||||||
|
err := json.Unmarshal(*errorRawParameters, ¶m)
|
||||||
|
if err != nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return ¶m
|
||||||
|
case "io.systemd.UserDatabase.BadService":
|
||||||
|
errorRawParameters := e.Parameters.(*json.RawMessage)
|
||||||
|
if errorRawParameters == nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
var param BadService
|
||||||
|
err := json.Unmarshal(*errorRawParameters, ¶m)
|
||||||
|
if err != nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return ¶m
|
||||||
|
case "io.systemd.UserDatabase.ServiceNotAvailable":
|
||||||
|
errorRawParameters := e.Parameters.(*json.RawMessage)
|
||||||
|
if errorRawParameters == nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
var param ServiceNotAvailable
|
||||||
|
err := json.Unmarshal(*errorRawParameters, ¶m)
|
||||||
|
if err != nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return ¶m
|
||||||
|
case "io.systemd.UserDatabase.ConflictingRecordFound":
|
||||||
|
errorRawParameters := e.Parameters.(*json.RawMessage)
|
||||||
|
if errorRawParameters == nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
var param ConflictingRecordFound
|
||||||
|
err := json.Unmarshal(*errorRawParameters, ¶m)
|
||||||
|
if err != nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return ¶m
|
||||||
|
case "io.systemd.UserDatabase.EnumerationNotSupported":
|
||||||
|
errorRawParameters := e.Parameters.(*json.RawMessage)
|
||||||
|
if errorRawParameters == nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
var param EnumerationNotSupported
|
||||||
|
err := json.Unmarshal(*errorRawParameters, ¶m)
|
||||||
|
if err != nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return ¶m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generated client method calls
|
||||||
|
|
||||||
|
type GetUserRecord_methods struct{}
|
||||||
|
|
||||||
|
func GetUserRecord() GetUserRecord_methods { return GetUserRecord_methods{} }
|
||||||
|
|
||||||
|
func (m GetUserRecord_methods) Call(ctx context.Context, c *varlink.Connection, uid_in_ *int64, userName_in_ *string, service_in_ string) (record_out_ json.RawMessage, incomplete_out_ bool, err_ error) {
|
||||||
|
receive, err_ := m.Send(ctx, c, 0, uid_in_, userName_in_, service_in_)
|
||||||
|
if err_ != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
record_out_, incomplete_out_, _, err_ = receive(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m GetUserRecord_methods) Send(ctx context.Context, c *varlink.Connection, flags uint64, uid_in_ *int64, userName_in_ *string, service_in_ string) (func(ctx context.Context) (json.RawMessage, bool, uint64, error), error) {
|
||||||
|
var in struct {
|
||||||
|
Uid *int64 `json:"uid,omitempty"`
|
||||||
|
UserName *string `json:"userName,omitempty"`
|
||||||
|
Service string `json:"service"`
|
||||||
|
}
|
||||||
|
in.Uid = uid_in_
|
||||||
|
in.UserName = userName_in_
|
||||||
|
in.Service = service_in_
|
||||||
|
receive, err := c.Send(ctx, "io.systemd.UserDatabase.GetUserRecord", in, flags)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return func(context.Context) (record_out_ json.RawMessage, incomplete_out_ bool, flags uint64, err error) {
|
||||||
|
var out struct {
|
||||||
|
Record json.RawMessage `json:"record"`
|
||||||
|
Incomplete bool `json:"incomplete"`
|
||||||
|
}
|
||||||
|
flags, err = receive(ctx, &out)
|
||||||
|
if err != nil {
|
||||||
|
err = Dispatch_Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
record_out_ = out.Record
|
||||||
|
incomplete_out_ = out.Incomplete
|
||||||
|
return
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m GetUserRecord_methods) Upgrade(ctx context.Context, c *varlink.Connection, uid_in_ *int64, userName_in_ *string, service_in_ string) (func(ctx context.Context) (record_out_ json.RawMessage, incomplete_out_ bool, flags uint64, conn varlink.ReadWriterContext, err_ error), error) {
|
||||||
|
var in struct {
|
||||||
|
Uid *int64 `json:"uid,omitempty"`
|
||||||
|
UserName *string `json:"userName,omitempty"`
|
||||||
|
Service string `json:"service"`
|
||||||
|
}
|
||||||
|
in.Uid = uid_in_
|
||||||
|
in.UserName = userName_in_
|
||||||
|
in.Service = service_in_
|
||||||
|
receive, err := c.Upgrade(ctx, "io.systemd.UserDatabase.GetUserRecord", in)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return func(context.Context) (record_out_ json.RawMessage, incomplete_out_ bool, flags uint64, conn varlink.ReadWriterContext, err error) {
|
||||||
|
var out struct {
|
||||||
|
Record json.RawMessage `json:"record"`
|
||||||
|
Incomplete bool `json:"incomplete"`
|
||||||
|
}
|
||||||
|
flags, conn, err = receive(ctx, &out)
|
||||||
|
if err != nil {
|
||||||
|
err = Dispatch_Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
record_out_ = out.Record
|
||||||
|
incomplete_out_ = out.Incomplete
|
||||||
|
return
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetGroupRecord_methods struct{}
|
||||||
|
|
||||||
|
func GetGroupRecord() GetGroupRecord_methods { return GetGroupRecord_methods{} }
|
||||||
|
|
||||||
|
func (m GetGroupRecord_methods) Call(ctx context.Context, c *varlink.Connection, gid_in_ *int64, groupName_in_ *string, service_in_ string) (record_out_ json.RawMessage, incomplete_out_ bool, err_ error) {
|
||||||
|
receive, err_ := m.Send(ctx, c, 0, gid_in_, groupName_in_, service_in_)
|
||||||
|
if err_ != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
record_out_, incomplete_out_, _, err_ = receive(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m GetGroupRecord_methods) Send(ctx context.Context, c *varlink.Connection, flags uint64, gid_in_ *int64, groupName_in_ *string, service_in_ string) (func(ctx context.Context) (json.RawMessage, bool, uint64, error), error) {
|
||||||
|
var in struct {
|
||||||
|
Gid *int64 `json:"gid,omitempty"`
|
||||||
|
GroupName *string `json:"groupName,omitempty"`
|
||||||
|
Service string `json:"service"`
|
||||||
|
}
|
||||||
|
in.Gid = gid_in_
|
||||||
|
in.GroupName = groupName_in_
|
||||||
|
in.Service = service_in_
|
||||||
|
receive, err := c.Send(ctx, "io.systemd.UserDatabase.GetGroupRecord", in, flags)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return func(context.Context) (record_out_ json.RawMessage, incomplete_out_ bool, flags uint64, err error) {
|
||||||
|
var out struct {
|
||||||
|
Record json.RawMessage `json:"record"`
|
||||||
|
Incomplete bool `json:"incomplete"`
|
||||||
|
}
|
||||||
|
flags, err = receive(ctx, &out)
|
||||||
|
if err != nil {
|
||||||
|
err = Dispatch_Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
record_out_ = out.Record
|
||||||
|
incomplete_out_ = out.Incomplete
|
||||||
|
return
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m GetGroupRecord_methods) Upgrade(ctx context.Context, c *varlink.Connection, gid_in_ *int64, groupName_in_ *string, service_in_ string) (func(ctx context.Context) (record_out_ json.RawMessage, incomplete_out_ bool, flags uint64, conn varlink.ReadWriterContext, err_ error), error) {
|
||||||
|
var in struct {
|
||||||
|
Gid *int64 `json:"gid,omitempty"`
|
||||||
|
GroupName *string `json:"groupName,omitempty"`
|
||||||
|
Service string `json:"service"`
|
||||||
|
}
|
||||||
|
in.Gid = gid_in_
|
||||||
|
in.GroupName = groupName_in_
|
||||||
|
in.Service = service_in_
|
||||||
|
receive, err := c.Upgrade(ctx, "io.systemd.UserDatabase.GetGroupRecord", in)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return func(context.Context) (record_out_ json.RawMessage, incomplete_out_ bool, flags uint64, conn varlink.ReadWriterContext, err error) {
|
||||||
|
var out struct {
|
||||||
|
Record json.RawMessage `json:"record"`
|
||||||
|
Incomplete bool `json:"incomplete"`
|
||||||
|
}
|
||||||
|
flags, conn, err = receive(ctx, &out)
|
||||||
|
if err != nil {
|
||||||
|
err = Dispatch_Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
record_out_ = out.Record
|
||||||
|
incomplete_out_ = out.Incomplete
|
||||||
|
return
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetMemberships_methods struct{}
|
||||||
|
|
||||||
|
func GetMemberships() GetMemberships_methods { return GetMemberships_methods{} }
|
||||||
|
|
||||||
|
func (m GetMemberships_methods) Call(ctx context.Context, c *varlink.Connection, userName_in_ *string, groupName_in_ *string, service_in_ string) (userName_out_ string, groupName_out_ string, err_ error) {
|
||||||
|
receive, err_ := m.Send(ctx, c, 0, userName_in_, groupName_in_, service_in_)
|
||||||
|
if err_ != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userName_out_, groupName_out_, _, err_ = receive(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m GetMemberships_methods) Send(ctx context.Context, c *varlink.Connection, flags uint64, userName_in_ *string, groupName_in_ *string, service_in_ string) (func(ctx context.Context) (string, string, uint64, error), error) {
|
||||||
|
var in struct {
|
||||||
|
UserName *string `json:"userName,omitempty"`
|
||||||
|
GroupName *string `json:"groupName,omitempty"`
|
||||||
|
Service string `json:"service"`
|
||||||
|
}
|
||||||
|
in.UserName = userName_in_
|
||||||
|
in.GroupName = groupName_in_
|
||||||
|
in.Service = service_in_
|
||||||
|
receive, err := c.Send(ctx, "io.systemd.UserDatabase.GetMemberships", in, flags)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return func(context.Context) (userName_out_ string, groupName_out_ string, flags uint64, err error) {
|
||||||
|
var out struct {
|
||||||
|
UserName string `json:"userName"`
|
||||||
|
GroupName string `json:"groupName"`
|
||||||
|
}
|
||||||
|
flags, err = receive(ctx, &out)
|
||||||
|
if err != nil {
|
||||||
|
err = Dispatch_Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userName_out_ = out.UserName
|
||||||
|
groupName_out_ = out.GroupName
|
||||||
|
return
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m GetMemberships_methods) Upgrade(ctx context.Context, c *varlink.Connection, userName_in_ *string, groupName_in_ *string, service_in_ string) (func(ctx context.Context) (userName_out_ string, groupName_out_ string, flags uint64, conn varlink.ReadWriterContext, err_ error), error) {
|
||||||
|
var in struct {
|
||||||
|
UserName *string `json:"userName,omitempty"`
|
||||||
|
GroupName *string `json:"groupName,omitempty"`
|
||||||
|
Service string `json:"service"`
|
||||||
|
}
|
||||||
|
in.UserName = userName_in_
|
||||||
|
in.GroupName = groupName_in_
|
||||||
|
in.Service = service_in_
|
||||||
|
receive, err := c.Upgrade(ctx, "io.systemd.UserDatabase.GetMemberships", in)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return func(context.Context) (userName_out_ string, groupName_out_ string, flags uint64, conn varlink.ReadWriterContext, err error) {
|
||||||
|
var out struct {
|
||||||
|
UserName string `json:"userName"`
|
||||||
|
GroupName string `json:"groupName"`
|
||||||
|
}
|
||||||
|
flags, conn, err = receive(ctx, &out)
|
||||||
|
if err != nil {
|
||||||
|
err = Dispatch_Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userName_out_ = out.UserName
|
||||||
|
groupName_out_ = out.GroupName
|
||||||
|
return
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generated service interface with all methods
|
||||||
|
|
||||||
|
type iosystemduserdatabaseInterface interface {
|
||||||
|
GetUserRecord(ctx context.Context, c VarlinkCall, uid_ *int64, userName_ *string, service_ string) error
|
||||||
|
GetGroupRecord(ctx context.Context, c VarlinkCall, gid_ *int64, groupName_ *string, service_ string) error
|
||||||
|
GetMemberships(ctx context.Context, c VarlinkCall, userName_ *string, groupName_ *string, service_ string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generated service object with all methods
|
||||||
|
|
||||||
|
type VarlinkCall struct{ varlink.Call }
|
||||||
|
|
||||||
|
// Generated reply methods for all varlink errors
|
||||||
|
|
||||||
|
func (c *VarlinkCall) ReplyNoRecordFound(ctx context.Context) error {
|
||||||
|
var out NoRecordFound
|
||||||
|
return c.ReplyError(ctx, "io.systemd.UserDatabase.NoRecordFound", &out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *VarlinkCall) ReplyBadService(ctx context.Context) error {
|
||||||
|
var out BadService
|
||||||
|
return c.ReplyError(ctx, "io.systemd.UserDatabase.BadService", &out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *VarlinkCall) ReplyServiceNotAvailable(ctx context.Context) error {
|
||||||
|
var out ServiceNotAvailable
|
||||||
|
return c.ReplyError(ctx, "io.systemd.UserDatabase.ServiceNotAvailable", &out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *VarlinkCall) ReplyConflictingRecordFound(ctx context.Context) error {
|
||||||
|
var out ConflictingRecordFound
|
||||||
|
return c.ReplyError(ctx, "io.systemd.UserDatabase.ConflictingRecordFound", &out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *VarlinkCall) ReplyEnumerationNotSupported(ctx context.Context) error {
|
||||||
|
var out EnumerationNotSupported
|
||||||
|
return c.ReplyError(ctx, "io.systemd.UserDatabase.EnumerationNotSupported", &out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generated reply methods for all varlink methods
|
||||||
|
|
||||||
|
func (c *VarlinkCall) ReplyGetUserRecord(ctx context.Context, record_ json.RawMessage, incomplete_ bool) error {
|
||||||
|
var out struct {
|
||||||
|
Record json.RawMessage `json:"record"`
|
||||||
|
Incomplete bool `json:"incomplete"`
|
||||||
|
}
|
||||||
|
out.Record = record_
|
||||||
|
out.Incomplete = incomplete_
|
||||||
|
return c.Reply(ctx, &out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *VarlinkCall) ReplyGetGroupRecord(ctx context.Context, record_ json.RawMessage, incomplete_ bool) error {
|
||||||
|
var out struct {
|
||||||
|
Record json.RawMessage `json:"record"`
|
||||||
|
Incomplete bool `json:"incomplete"`
|
||||||
|
}
|
||||||
|
out.Record = record_
|
||||||
|
out.Incomplete = incomplete_
|
||||||
|
return c.Reply(ctx, &out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *VarlinkCall) ReplyGetMemberships(ctx context.Context, userName_ string, groupName_ string) error {
|
||||||
|
var out struct {
|
||||||
|
UserName string `json:"userName"`
|
||||||
|
GroupName string `json:"groupName"`
|
||||||
|
}
|
||||||
|
out.UserName = userName_
|
||||||
|
out.GroupName = groupName_
|
||||||
|
return c.Reply(ctx, &out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generated dummy implementations for all varlink methods
|
||||||
|
|
||||||
|
func (s *VarlinkInterface) GetUserRecord(ctx context.Context, c VarlinkCall, uid_ *int64, userName_ *string, service_ string) error {
|
||||||
|
return c.ReplyMethodNotImplemented(ctx, "io.systemd.UserDatabase.GetUserRecord")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *VarlinkInterface) GetGroupRecord(ctx context.Context, c VarlinkCall, gid_ *int64, groupName_ *string, service_ string) error {
|
||||||
|
return c.ReplyMethodNotImplemented(ctx, "io.systemd.UserDatabase.GetGroupRecord")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *VarlinkInterface) GetMemberships(ctx context.Context, c VarlinkCall, userName_ *string, groupName_ *string, service_ string) error {
|
||||||
|
return c.ReplyMethodNotImplemented(ctx, "io.systemd.UserDatabase.GetMemberships")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generated method call dispatcher
|
||||||
|
|
||||||
|
func (s *VarlinkInterface) VarlinkDispatch(ctx context.Context, call varlink.Call, methodname string) error {
|
||||||
|
switch methodname {
|
||||||
|
case "GetUserRecord":
|
||||||
|
var in struct {
|
||||||
|
Uid *int64 `json:"uid,omitempty"`
|
||||||
|
UserName *string `json:"userName,omitempty"`
|
||||||
|
Service string `json:"service"`
|
||||||
|
}
|
||||||
|
err := call.GetParameters(&in)
|
||||||
|
if err != nil {
|
||||||
|
return call.ReplyInvalidParameter(ctx, "parameters")
|
||||||
|
}
|
||||||
|
return s.iosystemduserdatabaseInterface.GetUserRecord(ctx, VarlinkCall{call}, in.Uid, in.UserName, in.Service)
|
||||||
|
|
||||||
|
case "GetGroupRecord":
|
||||||
|
var in struct {
|
||||||
|
Gid *int64 `json:"gid,omitempty"`
|
||||||
|
GroupName *string `json:"groupName,omitempty"`
|
||||||
|
Service string `json:"service"`
|
||||||
|
}
|
||||||
|
err := call.GetParameters(&in)
|
||||||
|
if err != nil {
|
||||||
|
return call.ReplyInvalidParameter(ctx, "parameters")
|
||||||
|
}
|
||||||
|
return s.iosystemduserdatabaseInterface.GetGroupRecord(ctx, VarlinkCall{call}, in.Gid, in.GroupName, in.Service)
|
||||||
|
|
||||||
|
case "GetMemberships":
|
||||||
|
var in struct {
|
||||||
|
UserName *string `json:"userName,omitempty"`
|
||||||
|
GroupName *string `json:"groupName,omitempty"`
|
||||||
|
Service string `json:"service"`
|
||||||
|
}
|
||||||
|
err := call.GetParameters(&in)
|
||||||
|
if err != nil {
|
||||||
|
return call.ReplyInvalidParameter(ctx, "parameters")
|
||||||
|
}
|
||||||
|
return s.iosystemduserdatabaseInterface.GetMemberships(ctx, VarlinkCall{call}, in.UserName, in.GroupName, in.Service)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return call.ReplyMethodNotFound(ctx, methodname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generated varlink interface name
|
||||||
|
|
||||||
|
func (s *VarlinkInterface) VarlinkGetName() string {
|
||||||
|
return `io.systemd.UserDatabase`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generated varlink interface description
|
||||||
|
|
||||||
|
func (s *VarlinkInterface) VarlinkGetDescription() string {
|
||||||
|
return `interface io.systemd.UserDatabase
|
||||||
|
|
||||||
|
method GetUserRecord(
|
||||||
|
uid : ?int,
|
||||||
|
userName : ?string,
|
||||||
|
service : string
|
||||||
|
) -> (
|
||||||
|
record : object,
|
||||||
|
incomplete : bool
|
||||||
|
)
|
||||||
|
|
||||||
|
method GetGroupRecord(
|
||||||
|
gid : ?int,
|
||||||
|
groupName : ?string,
|
||||||
|
service : string
|
||||||
|
) -> (
|
||||||
|
record : object,
|
||||||
|
incomplete : bool
|
||||||
|
)
|
||||||
|
|
||||||
|
method GetMemberships(
|
||||||
|
userName : ?string,
|
||||||
|
groupName : ?string,
|
||||||
|
service : string
|
||||||
|
) -> (
|
||||||
|
userName : string,
|
||||||
|
groupName : string
|
||||||
|
)
|
||||||
|
|
||||||
|
error NoRecordFound()
|
||||||
|
error BadService()
|
||||||
|
error ServiceNotAvailable()
|
||||||
|
error ConflictingRecordFound()
|
||||||
|
error EnumerationNotSupported()
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generated service interface
|
||||||
|
|
||||||
|
type VarlinkInterface struct {
|
||||||
|
iosystemduserdatabaseInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func VarlinkNew(m iosystemduserdatabaseInterface) *VarlinkInterface {
|
||||||
|
return &VarlinkInterface{m}
|
||||||
|
}
|
71
internal/webui/act_chpw.go
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
package webui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/GehirnInc/crypt/sha512_crypt"
|
||||||
|
"go.e43.eu/authentricity/internal/models"
|
||||||
|
"go.e43.eu/authentricity/internal/store"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Service) actionChangePassword(
|
||||||
|
w http.ResponseWriter,
|
||||||
|
r *http.Request,
|
||||||
|
ent models.Entity,
|
||||||
|
md store.EntryMetadata,
|
||||||
|
) {
|
||||||
|
if ent.Type() != models.TypeUser {
|
||||||
|
s.renderBadRequest(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user := ent.(*models.UserRecord)
|
||||||
|
|
||||||
|
if !s.canEditEntity(r.Context(), user.UUID) {
|
||||||
|
s.renderForbidden(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tok := getUserToken(r.Context())
|
||||||
|
isYou := user.UUID.String() == tok.Subject()
|
||||||
|
|
||||||
|
priv := user.EnsurePrivileged()
|
||||||
|
oldPW := r.PostForm.Get("old_password")
|
||||||
|
newPW := r.PostForm.Get("new_password")
|
||||||
|
confirmPW := r.PostForm.Get("confirm_password")
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case newPW == "":
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
s.renderEntity(w, r, ent, "New password must not be blank")
|
||||||
|
return
|
||||||
|
case newPW != confirmPW:
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
s.renderEntity(w, r, ent, "Passwords do not match")
|
||||||
|
return
|
||||||
|
|
||||||
|
// Admins skip old password check, except for when changing their own password
|
||||||
|
case isYou && !priv.CheckPassword(oldPW):
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
s.renderEntity(w, r, ent, "Old password incorrect")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: This should probably be customizable!
|
||||||
|
c := sha512_crypt.New()
|
||||||
|
hash, err := c.Generate([]byte(newPW), nil)
|
||||||
|
if err != nil {
|
||||||
|
zap.L().Error("Error computing password hash", zap.Error(err))
|
||||||
|
s.renderError(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
priv.HashedPassword = []string{hash}
|
||||||
|
|
||||||
|
if err := s.store.UpdateEntitySimple(r.Context(), user, md); err != nil {
|
||||||
|
zap.L().Error("Error updating user", zap.Error(err))
|
||||||
|
s.renderError(w)
|
||||||
|
} else {
|
||||||
|
s.renderEntity(w, r, ent, "Password changed")
|
||||||
|
}
|
||||||
|
}
|
93
internal/webui/act_groups.go
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
package webui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"go.e43.eu/authentricity/internal/models"
|
||||||
|
"go.e43.eu/authentricity/internal/store"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Service) actionAddGroup(
|
||||||
|
w http.ResponseWriter,
|
||||||
|
r *http.Request,
|
||||||
|
ent models.Entity,
|
||||||
|
md store.EntryMetadata,
|
||||||
|
) {
|
||||||
|
if ent.Type() != models.TypeUser {
|
||||||
|
s.renderBadRequest(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user := ent.(*models.UserRecord)
|
||||||
|
|
||||||
|
if !s.isAdmin(r.Context()) {
|
||||||
|
s.renderForbidden(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
groupName := strings.TrimSpace(r.PostForm.Get("group"))
|
||||||
|
if groupName == "" {
|
||||||
|
s.renderEntity(w, r, ent, "No group specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
grent, _, err := store.GetEntityByAnyName(r.Context(), s.store, groupName)
|
||||||
|
if err != nil {
|
||||||
|
zap.L().Error("Error finding group", zap.Error(err))
|
||||||
|
s.renderError(w)
|
||||||
|
return
|
||||||
|
} else if grent == nil {
|
||||||
|
s.renderEntity(w, r, ent, "Group not found")
|
||||||
|
return
|
||||||
|
} else if grent.Type() != models.TypeGroup {
|
||||||
|
s.renderEntity(w, r, ent, "Not a group")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.store.AddUserToGroup(r.Context(), user.UUID, grent.ID())
|
||||||
|
if err != nil {
|
||||||
|
zap.L().Error("Error adding user to group", zap.Error(err))
|
||||||
|
s.renderError(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.renderEntity(w, r, ent, "Group added")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) actionRemoveGroups(
|
||||||
|
w http.ResponseWriter,
|
||||||
|
r *http.Request,
|
||||||
|
ent models.Entity,
|
||||||
|
md store.EntryMetadata,
|
||||||
|
) {
|
||||||
|
if ent.Type() != models.TypeUser {
|
||||||
|
s.renderBadRequest(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user := ent.(*models.UserRecord)
|
||||||
|
|
||||||
|
if !s.isAdmin(r.Context()) {
|
||||||
|
s.renderForbidden(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
toRemove := r.PostForm["group"]
|
||||||
|
for _, grp := range toRemove {
|
||||||
|
id, err := uuid.Parse(grp)
|
||||||
|
if err != nil {
|
||||||
|
zap.L().Error("Error parsing group ID", zap.Error(err))
|
||||||
|
s.renderError(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.store.RemoveUserFromGroup(r.Context(), user.UUID, id)
|
||||||
|
if err != nil {
|
||||||
|
zap.L().Error("Error removing user from group", zap.Error(err))
|
||||||
|
s.renderError(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.renderEntity(w, r, ent, "Groups removed")
|
||||||
|
}
|
97
internal/webui/act_ssh.go
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
package webui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"go.e43.eu/authentricity/internal/models"
|
||||||
|
"go.e43.eu/authentricity/internal/store"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Service) actionAddSSHKey(
|
||||||
|
w http.ResponseWriter,
|
||||||
|
r *http.Request,
|
||||||
|
ent models.Entity,
|
||||||
|
md store.EntryMetadata,
|
||||||
|
) {
|
||||||
|
if ent.Type() != models.TypeUser {
|
||||||
|
s.renderBadRequest(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user := ent.(*models.UserRecord)
|
||||||
|
|
||||||
|
if !s.canEditEntity(r.Context(), user.ID()) {
|
||||||
|
s.renderForbidden(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
key := strings.TrimSpace(r.PostForm.Get("key"))
|
||||||
|
if key == "" {
|
||||||
|
s.renderEntity(w, r, ent, "No SSH key Specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
priv := user.EnsurePrivileged()
|
||||||
|
found := false
|
||||||
|
for _, k := range priv.SSHAuthorizedKeys {
|
||||||
|
if k == key {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
priv.SSHAuthorizedKeys = append(priv.SSHAuthorizedKeys, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.store.UpdateEntitySimple(r.Context(), user, md); err != nil {
|
||||||
|
zap.L().Error("Error updating user", zap.Error(err))
|
||||||
|
s.renderError(w)
|
||||||
|
} else {
|
||||||
|
s.renderEntity(w, r, ent, "SSH Key Added")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) actionRemoveSSHKeys(
|
||||||
|
w http.ResponseWriter,
|
||||||
|
r *http.Request,
|
||||||
|
ent models.Entity,
|
||||||
|
md store.EntryMetadata,
|
||||||
|
) {
|
||||||
|
if ent.Type() != models.TypeUser {
|
||||||
|
s.renderBadRequest(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user := ent.(*models.UserRecord)
|
||||||
|
|
||||||
|
if !s.canEditEntity(r.Context(), user.UUID) {
|
||||||
|
s.renderForbidden(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
toRemove := r.PostForm["key"]
|
||||||
|
|
||||||
|
priv := user.EnsurePrivileged()
|
||||||
|
newKeys := []string{}
|
||||||
|
|
||||||
|
for _, k := range priv.SSHAuthorizedKeys {
|
||||||
|
remove := false
|
||||||
|
for _, kr := range toRemove {
|
||||||
|
if k == kr {
|
||||||
|
remove = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !remove {
|
||||||
|
newKeys = append(newKeys, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
priv.SSHAuthorizedKeys = newKeys
|
||||||
|
|
||||||
|
if err := s.store.UpdateEntitySimple(r.Context(), user, md); err != nil {
|
||||||
|
zap.L().Error("Error updating user", zap.Error(err))
|
||||||
|
s.renderError(w)
|
||||||
|
} else {
|
||||||
|
s.renderEntity(w, r, ent, "SSH Keys removed")
|
||||||
|
}
|
||||||
|
}
|
38
internal/webui/authz.go
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
package webui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Service) isInGroup(ctx context.Context, id uuid.UUID) bool {
|
||||||
|
tok := getUserToken(ctx)
|
||||||
|
groupsIfc, ok := tok.Get("authentricity.groups")
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
groups, ok := groupsIfc.([]interface{})
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
gid := id.String()
|
||||||
|
for _, g := range groups {
|
||||||
|
if g == gid {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) isAdmin(ctx context.Context) bool {
|
||||||
|
return s.isInGroup(ctx, s.adminGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) canEditEntity(ctx context.Context, id uuid.UUID) bool {
|
||||||
|
tok := getUserToken(ctx)
|
||||||
|
|
||||||
|
return tok.Subject() == id.String() || s.isAdmin(ctx)
|
||||||
|
}
|
43
internal/webui/content/base.tmpl
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
{{define "header"}}
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>{{.Title}}</title>
|
||||||
|
<link href="/static/bootstrap/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link href="/static/bootstrap-icons/bootstrap-icons.css" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<br>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-2">
|
||||||
|
<ul class="nav nav-pills flex-column">
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/">Your Account</a></li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<form action="/logout" method="POST">
|
||||||
|
<button class="nav-link" type="submit">Logout</button>
|
||||||
|
{{.CSRFField}}
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<main class="col-sm-10">
|
||||||
|
<div style="float: right">
|
||||||
|
<form action="/entity">
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input type="text" name="q" class="form-control" placeholder="username, e-mail address, ...">
|
||||||
|
<button type="submit" class="btn btn-outline-secondary"><i class="bi-search"></i></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{end}}
|
||||||
|
{{define "footer"}}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{end}}
|
18
internal/webui/content/ent_group.tmpl
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
{{template "header" .}}
|
||||||
|
<h1><i class="bi-people-fill"></i> {{.Name}}</h1>
|
||||||
|
<hr>
|
||||||
|
<h3>Basic Details</h2>
|
||||||
|
<table class="table">
|
||||||
|
<tr><th style="width: 200px">Groupname</th><td>{{.GroupName}}</td></tr>
|
||||||
|
<tr><th>Name</th><td>{{if .DisplayName}}{{.DisplayName}}{{else}}<i>Not set</i>{{end}}</td></tr>
|
||||||
|
<tr><th>Disposition</th><td>{{.Disposition}}</tr>
|
||||||
|
</table>
|
||||||
|
<h3>Users</h2>
|
||||||
|
<ul>
|
||||||
|
{{range .Users}}
|
||||||
|
<li><a href="/entity/{{.ID}}">{{.Name}}</a></li>
|
||||||
|
{{else}}
|
||||||
|
<li><i>None</i></li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
{{template "footer"}}
|
102
internal/webui/content/ent_user.tmpl
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
{{template "header" .}}
|
||||||
|
<h1><i class="{{if .IsYou}}bi-person-circle{{else}}bi-person-fill{{end}}"></i> {{.Name}}</h1>
|
||||||
|
<hr>
|
||||||
|
{{with .Message}}
|
||||||
|
<div class="alert alert-primary" role="alert">
|
||||||
|
{{.}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
<h3>Basic Details</h2>
|
||||||
|
<table class="table">
|
||||||
|
<tr><th style="width: 200px">Username</th><td>{{.UserName}}</td></tr>
|
||||||
|
<tr><th>E-Mail Address</th><td>{{if .Email}}{{.Email}}{{else}}<i>Not set</i>{{end}}</td></tr>
|
||||||
|
<tr><th>Name</th><td>{{if .DisplayName}}{{.DisplayName}}{{else}}<i>Not set</i>{{end}}</td></tr>
|
||||||
|
</table>
|
||||||
|
<h3>Groups</h2>
|
||||||
|
<form method="POST" id="remove_groups_form">
|
||||||
|
<ul>
|
||||||
|
{{range .Groups}}
|
||||||
|
<li>{{if $.IsAdmin}}<input type="checkbox" name="group" value="{{.ID}}"> {{end}}<a href="/entity/{{.ID}}">{{.Name}}</a></li>
|
||||||
|
{{else}}
|
||||||
|
<li><i>None</i></li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
<input type="hidden" name="action" value="remove_groups">
|
||||||
|
{{.CSRFField}}
|
||||||
|
</form>
|
||||||
|
{{if .IsAdmin}}
|
||||||
|
<div class="d-flex flex-row justify-content-between">
|
||||||
|
<div><button type="submit" class="btn btn-danger" form="remove_groups_form">Remove from selected groups</button></div>
|
||||||
|
<div style="float:right">
|
||||||
|
<form method="POST">
|
||||||
|
<input type="hidden" name="action" value="add_group">
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<span class="input-group-text" id="basic-addon1"><i class="bi-people"></i></span>
|
||||||
|
<input type="text" name="group" class="form-control" placeholder="group name">
|
||||||
|
<button type="submit" class="btn btn-primary">Add</button>
|
||||||
|
</div>
|
||||||
|
{{.CSRFField}}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<h3>SSH Keys</h3>
|
||||||
|
<div>
|
||||||
|
<form method="POST" id="remove_ssh_keys_form">
|
||||||
|
<input type="hidden" name="action" value="remove_ssh_keys">
|
||||||
|
<table class="table" style="overflow-wrap: anywhere;">
|
||||||
|
<tr><th scope="col"></th><th scope="col">#</th><th scope="col" style="width: 100%">Key</th></tr>
|
||||||
|
{{range $ix, $val := .SSHKeys}}
|
||||||
|
<tr>
|
||||||
|
<td>{{if $.Editable}}<input type="checkbox" name="key" value="{{$val}}">{{end}}</td>
|
||||||
|
<th scope="row">{{$ix}}</th>
|
||||||
|
<td><tt>{{$val}}</tt></td>
|
||||||
|
</tr>
|
||||||
|
{{else}}
|
||||||
|
<tr><td colspan="3">No SSH keys configured</td></tr>
|
||||||
|
{{end}}
|
||||||
|
</table>
|
||||||
|
{{if .Editable}}
|
||||||
|
{{.CSRFField}}
|
||||||
|
{{end}}
|
||||||
|
</form>
|
||||||
|
<div class="d-flex flex-row justify-content-between">
|
||||||
|
<div><button type="submit" class="btn btn-danger" form="remove_ssh_keys_form">Remove selected keys</button></div>
|
||||||
|
{{if .Editable}}
|
||||||
|
<form method="POST">
|
||||||
|
<input type="hidden" name="action" value="add_ssh_key">
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<span class="input-group-text" id="basic-addon1"><i class="bi-key"></i></span>
|
||||||
|
<input type="text" name="key" class="form-control" placeholder="ecdsa-sha2-nistp256 ...">
|
||||||
|
<button type="submit" class="btn btn-primary">Add</button>
|
||||||
|
</div>
|
||||||
|
{{.CSRFField}}
|
||||||
|
</form>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
{{if .Editable}}
|
||||||
|
<h3>Change Password</h3>
|
||||||
|
<form method="POST">
|
||||||
|
<input type="hidden" name="action" value="change_password">
|
||||||
|
{{if .IsYou}}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="oldPassword" class="form-label">Old Password</label>
|
||||||
|
<input type="password" class="form-control" id="oldPassword" name="old_password">
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="newPassword" class="form-label">New Password</label>
|
||||||
|
<input type="password" class="form-control" id="newPassword" name="new_password">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="confirmPassword" class="form-label">Confirm New Password</label>
|
||||||
|
<input type="password" class="form-control" id="confirmPassword" name="confirm_password">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Update</button>
|
||||||
|
{{.CSRFField}}
|
||||||
|
</form>
|
||||||
|
{{end}}
|
||||||
|
{{template "footer"}}
|
128
internal/webui/content/login.tmpl
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Login</title>
|
||||||
|
<link href="/static/bootstrap/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 40px;
|
||||||
|
padding-bottom: 40px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-signin {
|
||||||
|
max-width: 330px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-signin .form-floating:focus-within {
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-signin input[type="email"] {
|
||||||
|
margin-bottom: -1px;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-signin input[type="password"] {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-placeholder-img {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
text-anchor: middle;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.bd-placeholder-img-lg {
|
||||||
|
font-size: 3.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.b-example-divider {
|
||||||
|
height: 3rem;
|
||||||
|
background-color: rgba(0, 0, 0, .1);
|
||||||
|
border: solid rgba(0, 0, 0, .15);
|
||||||
|
border-width: 1px 0;
|
||||||
|
box-shadow: inset 0 .5em 1.5em rgba(0, 0, 0, .1), inset 0 .125em .5em rgba(0, 0, 0, .15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.b-example-vr {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bi {
|
||||||
|
vertical-align: -.125em;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-scroller {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
height: 2.75rem;
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-scroller .nav {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
margin-top: -1px;
|
||||||
|
overflow-x: auto;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="text-center">
|
||||||
|
<main class="form-signin w-100 m-auto">
|
||||||
|
<form action="/login" method="POST">
|
||||||
|
<!--<img class="mb-4" src="/docs/5.2/assets/brand/bootstrap-logo.svg" alt="" width="72" height="57">-->
|
||||||
|
<h1 class="h3 mb-3 fw-normal">Please sign in</h1>
|
||||||
|
|
||||||
|
{{if .ShowError}}
|
||||||
|
<div class="form-floating alert alert-warning" role="alert">
|
||||||
|
{{.ErrorMessage}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<div class="form-floating">
|
||||||
|
<input type="username" name="username" class="form-control" id="floatingInput" placeholder="bob">
|
||||||
|
<label for="floatingInput">Username or E-Mail Address</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-floating">
|
||||||
|
<input type="password" name="password" class="form-control" id="floatingPassword" placeholder="Password">
|
||||||
|
<label for="floatingPassword">Password</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="checkbox mb-3">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" value="remember-me"> Remember me
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button class="w-100 btn btn-lg btn-primary" type="submit">Sign in</button>
|
||||||
|
{{.CSRFField}}
|
||||||
|
<!--<p class="mt-5 mb-3 text-muted">© 2017–2022</p>-->
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
3
internal/webui/content/static/bootstrap-icons/123.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-123" viewBox="0 0 16 16">
|
||||||
|
<path d="M2.873 11.297V4.142H1.699L0 5.379v1.137l1.64-1.18h.06v5.961h1.174Zm3.213-5.09v-.063c0-.618.44-1.169 1.196-1.169.676 0 1.174.44 1.174 1.106 0 .624-.42 1.101-.807 1.526L4.99 10.553v.744h4.78v-.99H6.643v-.069L8.41 8.252c.65-.724 1.237-1.332 1.237-2.27C9.646 4.849 8.723 4 7.308 4c-1.573 0-2.36 1.064-2.36 2.15v.057h1.138Zm6.559 1.883h.786c.823 0 1.374.481 1.379 1.179.01.707-.55 1.216-1.421 1.21-.77-.005-1.326-.419-1.379-.953h-1.095c.042 1.053.938 1.918 2.464 1.918 1.478 0 2.642-.839 2.62-2.144-.02-1.143-.922-1.651-1.551-1.714v-.063c.535-.09 1.347-.66 1.326-1.678-.026-1.053-.933-1.855-2.359-1.845-1.5.005-2.317.88-2.348 1.898h1.116c.032-.498.498-.944 1.206-.944.703 0 1.206.435 1.206 1.07.005.64-.504 1.106-1.2 1.106h-.75v.96Z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 870 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-activity" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M6 2a.5.5 0 0 1 .47.33L10 12.036l1.53-4.208A.5.5 0 0 1 12 7.5h3.5a.5.5 0 0 1 0 1h-3.15l-1.88 5.17a.5.5 0 0 1-.94 0L6 3.964 4.47 8.171A.5.5 0 0 1 4 8.5H.5a.5.5 0 0 1 0-1h3.15l1.88-5.17A.5.5 0 0 1 6 2Z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 367 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-alarm-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M6 .5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1H9v1.07a7.001 7.001 0 0 1 3.274 12.474l.601.602a.5.5 0 0 1-.707.708l-.746-.746A6.97 6.97 0 0 1 8 16a6.97 6.97 0 0 1-3.422-.892l-.746.746a.5.5 0 0 1-.707-.708l.602-.602A7.001 7.001 0 0 1 7 2.07V1h-.5A.5.5 0 0 1 6 .5zm2.5 5a.5.5 0 0 0-1 0v3.362l-1.429 2.38a.5.5 0 1 0 .858.515l1.5-2.5A.5.5 0 0 0 8.5 9V5.5zM.86 5.387A2.5 2.5 0 1 1 4.387 1.86 8.035 8.035 0 0 0 .86 5.387zM11.613 1.86a2.5 2.5 0 1 1 3.527 3.527 8.035 8.035 0 0 0-3.527-3.527z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 626 B |
4
internal/webui/content/static/bootstrap-icons/alarm.svg
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-alarm" viewBox="0 0 16 16">
|
||||||
|
<path d="M8.5 5.5a.5.5 0 0 0-1 0v3.362l-1.429 2.38a.5.5 0 1 0 .858.515l1.5-2.5A.5.5 0 0 0 8.5 9V5.5z"/>
|
||||||
|
<path d="M6.5 0a.5.5 0 0 0 0 1H7v1.07a7.001 7.001 0 0 0-3.273 12.474l-.602.602a.5.5 0 0 0 .707.708l.746-.746A6.97 6.97 0 0 0 8 16a6.97 6.97 0 0 0 3.422-.892l.746.746a.5.5 0 0 0 .707-.708l-.601-.602A7.001 7.001 0 0 0 9 2.07V1h.5a.5.5 0 0 0 0-1h-3zm1.038 3.018a6.093 6.093 0 0 1 .924 0 6 6 0 1 1-.924 0zM0 3.5c0 .753.333 1.429.86 1.887A8.035 8.035 0 0 1 4.387 1.86 2.5 2.5 0 0 0 0 3.5zM13.5 1c-.753 0-1.429.333-1.887.86a8.035 8.035 0 0 1 3.527 3.527A2.5 2.5 0 0 0 13.5 1z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 711 B |
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-align-bottom" viewBox="0 0 16 16">
|
||||||
|
<rect width="4" height="12" x="6" y="1" rx="1"/>
|
||||||
|
<path d="M1.5 14a.5.5 0 0 0 0 1v-1zm13 1a.5.5 0 0 0 0-1v1zm-13 0h13v-1h-13v1z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 271 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-align-center" viewBox="0 0 16 16">
|
||||||
|
<path d="M8 1a.5.5 0 0 1 .5.5V6h-1V1.5A.5.5 0 0 1 8 1zm0 14a.5.5 0 0 1-.5-.5V10h1v4.5a.5.5 0 0 1-.5.5zM2 7a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V7z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 315 B |
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-align-end" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M14.5 1a.5.5 0 0 0-.5.5v13a.5.5 0 0 0 1 0v-13a.5.5 0 0 0-.5-.5z"/>
|
||||||
|
<path d="M13 7a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V7z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 318 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-align-middle" viewBox="0 0 16 16">
|
||||||
|
<path d="M6 13a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1v10zM1 8a.5.5 0 0 0 .5.5H6v-1H1.5A.5.5 0 0 0 1 8zm14 0a.5.5 0 0 1-.5.5H10v-1h4.5a.5.5 0 0 1 .5.5z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 316 B |
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-align-start" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M1.5 1a.5.5 0 0 1 .5.5v13a.5.5 0 0 1-1 0v-13a.5.5 0 0 1 .5-.5z"/>
|
||||||
|
<path d="M3 7a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V7z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 318 B |
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-align-top" viewBox="0 0 16 16">
|
||||||
|
<rect width="4" height="12" rx="1" transform="matrix(1 0 0 -1 6 15)"/>
|
||||||
|
<path d="M1.5 2a.5.5 0 0 1 0-1v1zm13-1a.5.5 0 0 1 0 1V1zm-13 0h13v1h-13V1z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 287 B |
3
internal/webui/content/static/bootstrap-icons/alt.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-alt" viewBox="0 0 16 16">
|
||||||
|
<path d="M1 13.5a.5.5 0 0 0 .5.5h3.797a.5.5 0 0 0 .439-.26L11 3h3.5a.5.5 0 0 0 0-1h-3.797a.5.5 0 0 0-.439.26L5 13H1.5a.5.5 0 0 0-.5.5zm10 0a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 0-1h-3a.5.5 0 0 0-.5.5z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 326 B |
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-app-indicator" viewBox="0 0 16 16">
|
||||||
|
<path d="M5.5 2A3.5 3.5 0 0 0 2 5.5v5A3.5 3.5 0 0 0 5.5 14h5a3.5 3.5 0 0 0 3.5-3.5V8a.5.5 0 0 1 1 0v2.5a4.5 4.5 0 0 1-4.5 4.5h-5A4.5 4.5 0 0 1 1 10.5v-5A4.5 4.5 0 0 1 5.5 1H8a.5.5 0 0 1 0 1H5.5z"/>
|
||||||
|
<path d="M16 3a3 3 0 1 1-6 0 3 3 0 0 1 6 0z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 387 B |
3
internal/webui/content/static/bootstrap-icons/app.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-app" viewBox="0 0 16 16">
|
||||||
|
<path d="M11 2a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h6zM5 1a4 4 0 0 0-4 4v6a4 4 0 0 0 4 4h6a4 4 0 0 0 4-4V5a4 4 0 0 0-4-4H5z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 282 B |
4
internal/webui/content/static/bootstrap-icons/apple.svg
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-apple" viewBox="0 0 16 16">
|
||||||
|
<path d="M11.182.008C11.148-.03 9.923.023 8.857 1.18c-1.066 1.156-.902 2.482-.878 2.516.024.034 1.52.087 2.475-1.258.955-1.345.762-2.391.728-2.43zm3.314 11.733c-.048-.096-2.325-1.234-2.113-3.422.212-2.189 1.675-2.789 1.698-2.854.023-.065-.597-.79-1.254-1.157a3.692 3.692 0 0 0-1.563-.434c-.108-.003-.483-.095-1.254.116-.508.139-1.653.589-1.968.607-.316.018-1.256-.522-2.267-.665-.647-.125-1.333.131-1.824.328-.49.196-1.422.754-2.074 2.237-.652 1.482-.311 3.83-.067 4.56.244.729.625 1.924 1.273 2.796.576.984 1.34 1.667 1.659 1.899.319.232 1.219.386 1.843.067.502-.308 1.408-.485 1.766-.472.357.013 1.061.154 1.782.539.571.197 1.111.115 1.652-.105.541-.221 1.324-1.059 2.238-2.758.347-.79.505-1.217.473-1.282z"/>
|
||||||
|
<path d="M11.182.008C11.148-.03 9.923.023 8.857 1.18c-1.066 1.156-.902 2.482-.878 2.516.024.034 1.52.087 2.475-1.258.955-1.345.762-2.391.728-2.43zm3.314 11.733c-.048-.096-2.325-1.234-2.113-3.422.212-2.189 1.675-2.789 1.698-2.854.023-.065-.597-.79-1.254-1.157a3.692 3.692 0 0 0-1.563-.434c-.108-.003-.483-.095-1.254.116-.508.139-1.653.589-1.968.607-.316.018-1.256-.522-2.267-.665-.647-.125-1.333.131-1.824.328-.49.196-1.422.754-2.074 2.237-.652 1.482-.311 3.83-.067 4.56.244.729.625 1.924 1.273 2.796.576.984 1.34 1.667 1.659 1.899.319.232 1.219.386 1.843.067.502-.308 1.408-.485 1.766-.472.357.013 1.061.154 1.782.539.571.197 1.111.115 1.652-.105.541-.221 1.324-1.059 2.238-2.758.347-.79.505-1.217.473-1.282z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-archive-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M12.643 15C13.979 15 15 13.845 15 12.5V5H1v7.5C1 13.845 2.021 15 3.357 15h9.286zM5.5 7h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1 0-1zM.8 1a.8.8 0 0 0-.8.8V3a.8.8 0 0 0 .8.8h14.4A.8.8 0 0 0 16 3V1.8a.8.8 0 0 0-.8-.8H.8z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 359 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-archive" viewBox="0 0 16 16">
|
||||||
|
<path d="M0 2a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1v7.5a2.5 2.5 0 0 1-2.5 2.5h-9A2.5 2.5 0 0 1 1 12.5V5a1 1 0 0 1-1-1V2zm2 3v7.5A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5V5H2zm13-3H1v2h14V2zM5 7.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 401 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-90deg-down" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M4.854 14.854a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 .708-.708L4 13.293V3.5A2.5 2.5 0 0 1 6.5 1h8a.5.5 0 0 1 0 1h-8A1.5 1.5 0 0 0 5 3.5v9.793l3.146-3.147a.5.5 0 0 1 .708.708l-4 4z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 350 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-90deg-left" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M1.146 4.854a.5.5 0 0 1 0-.708l4-4a.5.5 0 1 1 .708.708L2.707 4H12.5A2.5 2.5 0 0 1 15 6.5v8a.5.5 0 0 1-1 0v-8A1.5 1.5 0 0 0 12.5 5H2.707l3.147 3.146a.5.5 0 1 1-.708.708l-4-4z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 349 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-90deg-right" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M14.854 4.854a.5.5 0 0 0 0-.708l-4-4a.5.5 0 0 0-.708.708L13.293 4H3.5A2.5 2.5 0 0 0 1 6.5v8a.5.5 0 0 0 1 0v-8A1.5 1.5 0 0 1 3.5 5h9.793l-3.147 3.146a.5.5 0 0 0 .708.708l4-4z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 350 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-90deg-up" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M4.854 1.146a.5.5 0 0 0-.708 0l-4 4a.5.5 0 1 0 .708.708L4 2.707V12.5A2.5 2.5 0 0 0 6.5 15h8a.5.5 0 0 0 0-1h-8A1.5 1.5 0 0 1 5 12.5V2.707l3.146 3.147a.5.5 0 1 0 .708-.708l-4-4z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 349 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-bar-down" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M1 3.5a.5.5 0 0 1 .5-.5h13a.5.5 0 0 1 0 1h-13a.5.5 0 0 1-.5-.5zM8 6a.5.5 0 0 1 .5.5v5.793l2.146-2.147a.5.5 0 0 1 .708.708l-3 3a.5.5 0 0 1-.708 0l-3-3a.5.5 0 0 1 .708-.708L7.5 12.293V6.5A.5.5 0 0 1 8 6z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 375 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-bar-left" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M12.5 15a.5.5 0 0 1-.5-.5v-13a.5.5 0 0 1 1 0v13a.5.5 0 0 1-.5.5zM10 8a.5.5 0 0 1-.5.5H3.707l2.147 2.146a.5.5 0 0 1-.708.708l-3-3a.5.5 0 0 1 0-.708l3-3a.5.5 0 1 1 .708.708L3.707 7.5H9.5a.5.5 0 0 1 .5.5z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 375 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-bar-right" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M6 8a.5.5 0 0 0 .5.5h5.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3a.5.5 0 0 0 0-.708l-3-3a.5.5 0 0 0-.708.708L12.293 7.5H6.5A.5.5 0 0 0 6 8zm-2.5 7a.5.5 0 0 1-.5-.5v-13a.5.5 0 0 1 1 0v13a.5.5 0 0 1-.5.5z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 375 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-bar-up" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M8 10a.5.5 0 0 0 .5-.5V3.707l2.146 2.147a.5.5 0 0 0 .708-.708l-3-3a.5.5 0 0 0-.708 0l-3 3a.5.5 0 1 0 .708.708L7.5 3.707V9.5a.5.5 0 0 0 .5.5zm-7 2.5a.5.5 0 0 1 .5-.5h13a.5.5 0 0 1 0 1h-13a.5.5 0 0 1-.5-.5z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 376 B |
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-clockwise" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/>
|
||||||
|
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 352 B |
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-counterclockwise" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M8 3a5 5 0 1 1-4.546 2.914.5.5 0 0 0-.908-.417A6 6 0 1 0 8 2v1z"/>
|
||||||
|
<path d="M8 4.466V.534a.25.25 0 0 0-.41-.192L5.23 2.308a.25.25 0 0 0 0 .384l2.36 1.966A.25.25 0 0 0 8 4.466z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 359 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-down-circle-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM8.5 4.5a.5.5 0 0 0-1 0v5.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V4.5z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 321 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-down-circle" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8zm15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM8.5 4.5a.5.5 0 0 0-1 0v5.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V4.5z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 370 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-down-left-circle-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M16 8A8 8 0 1 0 0 8a8 8 0 0 0 16 0zm-5.904-2.803a.5.5 0 1 1 .707.707L6.707 10h2.768a.5.5 0 0 1 0 1H5.5a.5.5 0 0 1-.5-.5V6.525a.5.5 0 0 1 1 0v2.768l4.096-4.096z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 326 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-down-left-circle" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8zm15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-5.904-2.854a.5.5 0 1 1 .707.708L6.707 9.95h2.768a.5.5 0 1 1 0 1H5.5a.5.5 0 0 1-.5-.5V6.475a.5.5 0 1 1 1 0v2.768l4.096-4.097z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 377 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-down-left-square-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M2 16a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2zm8.096-10.803L6 9.293V6.525a.5.5 0 0 0-1 0V10.5a.5.5 0 0 0 .5.5h3.975a.5.5 0 0 0 0-1H6.707l4.096-4.096a.5.5 0 1 0-.707-.707z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 363 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-down-left-square" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M15 2a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V2zM0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2zm10.096 3.146a.5.5 0 1 1 .707.708L6.707 9.95h2.768a.5.5 0 1 1 0 1H5.5a.5.5 0 0 1-.5-.5V6.475a.5.5 0 1 1 1 0v2.768l4.096-4.097z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 451 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-down-left" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M2 13.5a.5.5 0 0 0 .5.5h6a.5.5 0 0 0 0-1H3.707L13.854 2.854a.5.5 0 0 0-.708-.708L3 12.293V7.5a.5.5 0 0 0-1 0v6z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 286 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-down-right-circle-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm5.904-2.803a.5.5 0 1 0-.707.707L9.293 10H6.525a.5.5 0 0 0 0 1H10.5a.5.5 0 0 0 .5-.5V6.525a.5.5 0 0 0-1 0v2.768L5.904 5.197z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 326 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-down-right-circle" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8zm15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.854 5.146a.5.5 0 1 0-.708.708L9.243 9.95H6.475a.5.5 0 1 0 0 1h3.975a.5.5 0 0 0 .5-.5V6.475a.5.5 0 1 0-1 0v2.768L5.854 5.146z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 379 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-down-right-square-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M14 16a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12zM5.904 5.197 10 9.293V6.525a.5.5 0 0 1 1 0V10.5a.5.5 0 0 1-.5.5H6.525a.5.5 0 0 1 0-1h2.768L5.197 5.904a.5.5 0 0 1 .707-.707z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 365 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-down-right-square" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M15 2a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V2zM0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2zm5.854 3.146a.5.5 0 1 0-.708.708L9.243 9.95H6.475a.5.5 0 1 0 0 1h3.975a.5.5 0 0 0 .5-.5V6.475a.5.5 0 1 0-1 0v2.768L5.854 5.146z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 453 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-down-right" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M14 13.5a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1 0-1h4.793L2.146 2.854a.5.5 0 1 1 .708-.708L13 12.293V7.5a.5.5 0 0 1 1 0v6z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 289 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-down-short" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M8 4a.5.5 0 0 1 .5.5v5.793l2.146-2.147a.5.5 0 0 1 .708.708l-3 3a.5.5 0 0 1-.708 0l-3-3a.5.5 0 1 1 .708-.708L7.5 10.293V4.5A.5.5 0 0 1 8 4z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 314 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-down-square-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v5.793l2.146-2.147a.5.5 0 0 1 .708.708l-3 3a.5.5 0 0 1-.708 0l-3-3a.5.5 0 1 1 .708-.708L7.5 10.293V4.5a.5.5 0 0 1 1 0z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 359 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-down-square" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M15 2a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V2zM0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2zm8.5 2.5a.5.5 0 0 0-1 0v5.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V4.5z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 444 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-down-up" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M11.5 15a.5.5 0 0 0 .5-.5V2.707l3.146 3.147a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0l-4 4a.5.5 0 1 0 .708.708L11 2.707V14.5a.5.5 0 0 0 .5.5zm-7-14a.5.5 0 0 1 .5.5v11.793l3.146-3.147a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 .708-.708L4 13.293V1.5a.5.5 0 0 1 .5-.5z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 457 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-down" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M8 1a.5.5 0 0 1 .5.5v11.793l3.146-3.147a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 .708-.708L7.5 13.293V1.5A.5.5 0 0 1 8 1z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 309 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-left-circle-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M8 0a8 8 0 1 0 0 16A8 8 0 0 0 8 0zm3.5 7.5a.5.5 0 0 1 0 1H5.707l2.147 2.146a.5.5 0 0 1-.708.708l-3-3a.5.5 0 0 1 0-.708l3-3a.5.5 0 1 1 .708.708L5.707 7.5H11.5z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 320 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-left-circle" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8zm15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-4.5-.5a.5.5 0 0 1 0 1H5.707l2.147 2.146a.5.5 0 0 1-.708.708l-3-3a.5.5 0 0 1 0-.708l3-3a.5.5 0 1 1 .708.708L5.707 7.5H11.5z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 370 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-left-right" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M1 11.5a.5.5 0 0 0 .5.5h11.793l-3.147 3.146a.5.5 0 0 0 .708.708l4-4a.5.5 0 0 0 0-.708l-4-4a.5.5 0 0 0-.708.708L13.293 11H1.5a.5.5 0 0 0-.5.5zm14-7a.5.5 0 0 1-.5.5H2.707l3.147 3.146a.5.5 0 1 1-.708.708l-4-4a.5.5 0 0 1 0-.708l4-4a.5.5 0 1 1 .708.708L2.707 4H14.5a.5.5 0 0 1 .5.5z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 453 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-left-short" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M12 8a.5.5 0 0 1-.5.5H5.707l2.147 2.146a.5.5 0 0 1-.708.708l-3-3a.5.5 0 0 1 0-.708l3-3a.5.5 0 1 1 .708.708L5.707 7.5H11.5a.5.5 0 0 1 .5.5z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 314 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-left-square-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M16 14a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12zm-4.5-6.5H5.707l2.147-2.146a.5.5 0 1 0-.708-.708l-3 3a.5.5 0 0 0 0 .708l3 3a.5.5 0 0 0 .708-.708L5.707 8.5H11.5a.5.5 0 0 0 0-1z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 362 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-left-square" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M15 2a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V2zM0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2zm11.5 5.5a.5.5 0 0 1 0 1H5.707l2.147 2.146a.5.5 0 0 1-.708.708l-3-3a.5.5 0 0 1 0-.708l3-3a.5.5 0 1 1 .708.708L5.707 7.5H11.5z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 445 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-left" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 311 B |
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-repeat" viewBox="0 0 16 16">
|
||||||
|
<path d="M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41zm-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9z"/>
|
||||||
|
<path fill-rule="evenodd" d="M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5.002 5.002 0 0 0 8 3zM3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9H3.1z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 582 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-return-left" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M14.5 1.5a.5.5 0 0 1 .5.5v4.8a2.5 2.5 0 0 1-2.5 2.5H2.707l3.347 3.346a.5.5 0 0 1-.708.708l-4.2-4.2a.5.5 0 0 1 0-.708l4-4a.5.5 0 1 1 .708.708L2.707 8.3H12.5A1.5 1.5 0 0 0 14 6.8V2a.5.5 0 0 1 .5-.5z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 373 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-return-right" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M1.5 1.5A.5.5 0 0 0 1 2v4.8a2.5 2.5 0 0 0 2.5 2.5h9.793l-3.347 3.346a.5.5 0 0 0 .708.708l4.2-4.2a.5.5 0 0 0 0-.708l-4-4a.5.5 0 0 0-.708.708L13.293 8.3H3.5A1.5 1.5 0 0 1 2 6.8V2a.5.5 0 0 0-.5-.5z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 372 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-right-circle-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0zM4.5 7.5a.5.5 0 0 0 0 1h5.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3a.5.5 0 0 0 0-.708l-3-3a.5.5 0 1 0-.708.708L10.293 7.5H4.5z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 322 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-right-circle" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8zm15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM4.5 7.5a.5.5 0 0 0 0 1h5.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3a.5.5 0 0 0 0-.708l-3-3a.5.5 0 1 0-.708.708L10.293 7.5H4.5z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 372 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-right-short" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M4 8a.5.5 0 0 1 .5-.5h5.793L8.146 5.354a.5.5 0 1 1 .708-.708l3 3a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708-.708L10.293 8.5H4.5A.5.5 0 0 1 4 8z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 316 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-right-square-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M0 14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2a2 2 0 0 0-2 2v12zm4.5-6.5h5.793L8.146 5.354a.5.5 0 1 1 .708-.708l3 3a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708-.708L10.293 8.5H4.5a.5.5 0 0 1 0-1z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 361 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-right-square" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M15 2a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V2zM0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2zm4.5 5.5a.5.5 0 0 0 0 1h5.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3a.5.5 0 0 0 0-.708l-3-3a.5.5 0 1 0-.708.708L10.293 7.5H4.5z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 446 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-right" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M1 8a.5.5 0 0 1 .5-.5h11.793l-3.147-3.146a.5.5 0 0 1 .708-.708l4 4a.5.5 0 0 1 0 .708l-4 4a.5.5 0 0 1-.708-.708L13.293 8.5H1.5A.5.5 0 0 1 1 8z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 312 B |