Initial WIP import

This commit is contained in:
Erin Shepherd 2022-07-11 21:49:26 +00:00
commit 5f68fa9d67
1774 changed files with 87669 additions and 0 deletions

5
COPYING Normal file
View 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
View 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)

View file

@ -0,0 +1,7 @@
package main
import "go.e43.eu/authentricity/internal/admintool"
func main() {
admintool.Main()
}

View file

@ -0,0 +1,7 @@
package main
import "go.e43.eu/authentricity/internal/hostagent"
func main() {
hostagent.Main()
}

View file

@ -0,0 +1,7 @@
package main
import "go.e43.eu/authentricity/internal/webui"
func main() {
webui.Main()
}

53
go.mod Normal file
View 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
View 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=

View 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)
}

View 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
View 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
View 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
View 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)
}

View 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)
})
})
}

View 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)
}
}

View 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
}

View 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))
}

View 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
View 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
View 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
View 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
View 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
View 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
}

View 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
}

View 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)
}

View 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
View 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
View 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
}

View file

@ -0,0 +1 @@
../../../varlink/eu.e43.authentricity.HostAgent.varlink

View file

@ -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, &param)
if err != nil {
return e
}
return &param
case "eu.e43.authentricity.HostAgent.NoRecordFound":
errorRawParameters := e.Parameters.(*json.RawMessage)
if errorRawParameters == nil {
return e
}
var param NoRecordFound
err := json.Unmarshal(*errorRawParameters, &param)
if err != nil {
return e
}
return &param
case "eu.e43.authentricity.HostAgent.MoreRequired":
errorRawParameters := e.Parameters.(*json.RawMessage)
if errorRawParameters == nil {
return e
}
var param MoreRequired
err := json.Unmarshal(*errorRawParameters, &param)
if err != nil {
return e
}
return &param
case "eu.e43.authentricity.HostAgent.ConflictingRecordFound":
errorRawParameters := e.Parameters.(*json.RawMessage)
if errorRawParameters == nil {
return e
}
var param ConflictingRecordFound
err := json.Unmarshal(*errorRawParameters, &param)
if err != nil {
return e
}
return &param
}
}
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}
}

View 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

View file

@ -0,0 +1,3 @@
package iosystemduserdatabase
//go:generate go run github.com/varlink/go/cmd/varlink-go-interface-generator io.systemd.UserDatabase.varlink

View file

@ -0,0 +1 @@
../../../varlink/io.systemd.UserDatabase.varlink

View 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, &param)
if err != nil {
return e
}
return &param
case "io.systemd.UserDatabase.BadService":
errorRawParameters := e.Parameters.(*json.RawMessage)
if errorRawParameters == nil {
return e
}
var param BadService
err := json.Unmarshal(*errorRawParameters, &param)
if err != nil {
return e
}
return &param
case "io.systemd.UserDatabase.ServiceNotAvailable":
errorRawParameters := e.Parameters.(*json.RawMessage)
if errorRawParameters == nil {
return e
}
var param ServiceNotAvailable
err := json.Unmarshal(*errorRawParameters, &param)
if err != nil {
return e
}
return &param
case "io.systemd.UserDatabase.ConflictingRecordFound":
errorRawParameters := e.Parameters.(*json.RawMessage)
if errorRawParameters == nil {
return e
}
var param ConflictingRecordFound
err := json.Unmarshal(*errorRawParameters, &param)
if err != nil {
return e
}
return &param
case "io.systemd.UserDatabase.EnumerationNotSupported":
errorRawParameters := e.Parameters.(*json.RawMessage)
if errorRawParameters == nil {
return e
}
var param EnumerationNotSupported
err := json.Unmarshal(*errorRawParameters, &param)
if err != nil {
return e
}
return &param
}
}
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}
}

View 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")
}
}

View 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
View 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
View 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)
}

View 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}}

View 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"}}

View 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"}}

View 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">&copy; 20172022</p>-->
</form>
</main>
</body>
</html>

View 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

View file

@ -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

View file

@ -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

View 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View 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

View file

@ -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

View 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

View 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

Some files were not shown because too many files have changed in this diff Show more