Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into sys-config-state
Browse files Browse the repository at this point in the history
  • Loading branch information
calvn committed Oct 7, 2019
2 parents da85f38 + 3a07094 commit 219d6ee
Show file tree
Hide file tree
Showing 235 changed files with 31,456 additions and 114 deletions.
15 changes: 8 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,16 @@ IMPROVEMENTS:
* auth/jwt: The redirect callback host may now be specified for CLI logins
[JWT-71]
* core: Exit ScanView if context has been cancelled [GH-7419]
* replication (enterprise): Write-Ahead-Log entries will not duplicate the
data belonging to the encompassing physical entries of the transaction,
thereby improving the performance and storage capacity.
* secrets/aws: The root config can now be read [GH-7245]
* storage/cassandra: Improve storage efficiency by eliminating unnecessary
copies of value data [GH-7199]
* sys: Add a new `sys/host-info` endpoint for querying information about
the host [GH-7330]
* sys: Add a new set of endpoints under `sys/pprof/` that allows profiling
information to be extracted [GH-7473]
* replication (enterprise): Write-Ahead-Log entries will not duplicate the
data belonging to the encompassing physical entries of the transaction,
thereby improving the performance and storage capacity.

BUG FIXES:

Expand All @@ -43,10 +43,11 @@ BUG FIXES:
`.well-known/openid-configuration` response [GH-7533]
* identity (enterprise): Fixed identity case sensitive loading in secondary
cluster [GH-7327]
* secrets/database: Fix bug in combined DB secrets engine that can result in
writes to static-roles endpoints timing out [GH-7518]
* ui: using the `wrapped_token` query param will work with `redirect_to` and
will automatically log in as intended [GH-7398]
* secret/database: Fix bug in combined DB secrets engine that can result in
writes to static-roles endpoints timing out [GH-7518]
* ui: Allow kv v2 secrets that are gated by Control Groups to be viewed in the UI [GH-7504]

## 1.2.3 (September 12, 2019)

Expand Down Expand Up @@ -285,13 +286,13 @@ BUG FIXES:
* namespaces: Fix a behavior (currently only known to be benign) where we
wouldn't delete policies through the official functions before wiping the
namespaces on deletion
* secrets/database: Escape username/password before using in connection URL
[GH-7089]
* secrets/pki: Forward revocation requests to active node when on a
performance standby [GH-7173]
* ui: Fix timestamp on some transit keys [GH-6827]
* ui: Show Entities and Groups in Side Navigation [GH-7138]
* ui: Ensure dropdown updates selected item on HTTP Request Metrics page
* secret/database: Escape username/password before using in connection URL
[GH-7089]

## 1.1.4/1.1.5 (July 25th/30th, 2019)

Expand Down
1 change: 0 additions & 1 deletion api/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ require (
github.com/hashicorp/hcl v1.0.0
github.com/hashicorp/vault/sdk v0.1.14-0.20190919081434-645ac174deeb
github.com/mitchellh/mapstructure v1.1.2
github.com/shirou/gopsutil v0.0.0-20190731134726-d80c43f9c984 // indirect
golang.org/x/net v0.0.0-20190620200207-3b0461eec859
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4
gopkg.in/square/go-jose.v2 v2.3.1
Expand Down
6 changes: 0 additions & 6 deletions api/go.sum
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
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/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
Expand All @@ -11,7 +10,6 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
Expand Down Expand Up @@ -69,9 +67,6 @@ github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndr
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/shirou/gopsutil v0.0.0-20190731134726-d80c43f9c984 h1:wsZAb4P8F7uQSwsnxE1gk9AHCcc5U0wvyDzcLwFY0Eo=
github.com/shirou/gopsutil v0.0.0-20190731134726-d80c43f9c984/go.mod h1:WWnYX4lzhCH5h/3YBfyVA3VbLYjlMZZAQcW9ojMexNc=
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
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 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
Expand All @@ -97,7 +92,6 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190423024810-112230192c58/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-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/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-20190403152447-81d4e9dc473e h1:nFYrTHrdrAOpShe27kaFHjsqYSEQ0KWqdWLu3xuZJts=
Expand Down
4 changes: 0 additions & 4 deletions command/kv_get.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,6 @@ func (c *KVGetCommand) Run(args []string) int {

if v2 {
path = addPrefixToVKVPath(path, mountPath, "data")
if err != nil {
c.UI.Error(err.Error())
return 2
}

if c.flagVersion > 0 {
versionParam = map[string]string{
Expand Down
58 changes: 39 additions & 19 deletions command/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,22 @@ import (
"sync"
"time"

"golang.org/x/net/http/httpproxy"

"github.com/hashicorp/vault/helper/metricsutil"

monitoring "cloud.google.com/go/monitoring/apiv3"
metrics "github.com/armon/go-metrics"
"github.com/armon/go-metrics"
"github.com/armon/go-metrics/circonus"
"github.com/armon/go-metrics/datadog"
"github.com/armon/go-metrics/prometheus"
stackdriver "github.com/google/go-metrics-stackdriver"
"github.com/hashicorp/errwrap"
log "github.com/hashicorp/go-hclog"
multierror "github.com/hashicorp/go-multierror"
sockaddr "github.com/hashicorp/go-sockaddr"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/go-sockaddr"
"github.com/hashicorp/vault/audit"
"github.com/hashicorp/vault/command/server"
serverseal "github.com/hashicorp/vault/command/server/seal"
"github.com/hashicorp/vault/helper/builtinplugins"
gatedwriter "github.com/hashicorp/vault/helper/gated-writer"
"github.com/hashicorp/vault/helper/metricsutil"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/helper/reload"
vaulthttp "github.com/hashicorp/vault/http"
Expand All @@ -54,8 +51,9 @@ import (
vaultseal "github.com/hashicorp/vault/vault/seal"
shamirseal "github.com/hashicorp/vault/vault/seal/shamir"
"github.com/mitchellh/cli"
testing "github.com/mitchellh/go-testing-interface"
"github.com/mitchellh/go-testing-interface"
"github.com/posener/complete"
"golang.org/x/net/http/httpproxy"
"google.golang.org/api/option"
"google.golang.org/grpc/grpclog"
)
Expand Down Expand Up @@ -125,9 +123,10 @@ type ServerCommand struct {

type ServerListener struct {
net.Listener
config map[string]interface{}
maxRequestSize int64
maxRequestDuration time.Duration
config map[string]interface{}
maxRequestSize int64
maxRequestDuration time.Duration
unauthenticatedMetricsAccess bool
}

func (c *ServerCommand) Synopsis() string {
Expand Down Expand Up @@ -989,11 +988,31 @@ CLUSTER_SYNTHESIS_COMPLETE:
}
props["max_request_duration"] = fmt.Sprintf("%s", maxRequestDuration.String())

var unauthenticatedMetricsAccess bool
if telemetryRaw, ok := lnConfig.Config["telemetry"]; ok {
telemetry, ok := telemetryRaw.([]map[string]interface{})
if !ok {
c.UI.Error(fmt.Sprintf("Could not parse telemetry sink value %v", telemetryRaw))
return 1
}

for _, item := range telemetry {
if valRaw, ok := item["unauthenticated_metrics_access"]; ok {
unauthenticatedMetricsAccess, err = parseutil.ParseBool(valRaw)
if err != nil {
c.UI.Error(fmt.Sprintf("Could not parse unauthenticated_metrics_access value %v", valRaw))
return 1
}
}
}
}

lns = append(lns, ServerListener{
Listener: ln,
config: lnConfig.Config,
maxRequestSize: maxRequestSize,
maxRequestDuration: maxRequestDuration,
Listener: ln,
config: lnConfig.Config,
maxRequestSize: maxRequestSize,
maxRequestDuration: maxRequestDuration,
unauthenticatedMetricsAccess: unauthenticatedMetricsAccess,
})

// Store the listener props for output later
Expand Down Expand Up @@ -1230,10 +1249,11 @@ CLUSTER_SYNTHESIS_COMPLETE:
// Initialize the HTTP servers
for _, ln := range lns {
handler := vaulthttp.Handler(&vault.HandlerProperties{
Core: core,
MaxRequestSize: ln.maxRequestSize,
MaxRequestDuration: ln.maxRequestDuration,
DisablePrintableCheck: config.DisablePrintableCheck,
Core: core,
MaxRequestSize: ln.maxRequestSize,
MaxRequestDuration: ln.maxRequestDuration,
DisablePrintableCheck: config.DisablePrintableCheck,
UnauthenticatedMetricsAccess: ln.unauthenticatedMetricsAccess,
})

// We perform validation on the config earlier, we can just cast here
Expand Down
5 changes: 3 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ github.com/SAP/go-hdb v0.14.1 h1:hkw4ozGZ/i4eak7ZuGkY5e0hxiXFdNUBNhr4AvZVNFE=
github.com/SAP/go-hdb v0.14.1/go.mod h1:7fdQLVC2lER3urZLjZCm0AuMQfApof92n3aylBPEkMo=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 h1:fLjPD/aNc3UIOA6tDi6QXUemppXK3P9BI7mr2hd6gx8=
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/abdullin/seq v0.0.0-20160510034733-d5467c17e7af h1:DBNMBMuMiWYu0b+8KMJuWmfCkcxl09JwdlqwDZZ6U14=
github.com/abdullin/seq v0.0.0-20160510034733-d5467c17e7af/go.mod h1:5Jv4cbFiHJMsVxt52+i0Ha45fjshj6wxYr1r19tB9bw=
Expand Down Expand Up @@ -168,6 +169,7 @@ github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab h1:xveKWz2iaueeTaUgdetzel+U7exyigDYBryyVfV/rZk=
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
github.com/go-ole/go-ole v1.2.1 h1:2lOsA72HgjxAuMlKpFiCbHTvu44PIVkZ5hqm3RSdI/E=
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
Expand Down Expand Up @@ -555,8 +557,7 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUt
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shirou/gopsutil v0.0.0-20190731134726-d80c43f9c984 h1:wsZAb4P8F7uQSwsnxE1gk9AHCcc5U0wvyDzcLwFY0Eo=
github.com/shirou/gopsutil v0.0.0-20190731134726-d80c43f9c984/go.mod h1:WWnYX4lzhCH5h/3YBfyVA3VbLYjlMZZAQcW9ojMexNc=
github.com/shirou/gopsutil v2.18.12+incompatible h1:1eaJvGomDnH74/5cF4CTmTbLHAriGFsTZppLXDX93OM=
github.com/shirou/gopsutil v2.18.12+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 h1:udFKJ0aHUL60LboW/A+DfgoHVedieIzIXE8uylPue0U=
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 h1:pntxY8Ary0t43dCZ5dqY4YTJCObLY1kIXl0uzMv+7DE=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
Expand Down
5 changes: 5 additions & 0 deletions http/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,11 @@ func Handler(props *vault.HandlerProperties) http.Handler {
mux.Handle("/", handleUIRedirect())
}

// Register metrics path without authentication if enabled
if props.UnauthenticatedMetricsAccess {
mux.Handle("/v1/sys/metrics", handleMetricsUnauthenticated(core))
}

additionalRoutes(mux, core)

// Wrap the handler in another handler to trigger all help paths.
Expand Down
32 changes: 32 additions & 0 deletions http/sys_metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package http

import (
"net/http"

"github.com/hashicorp/vault/helper/metricsutil"
"github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/vault/vault"
)

func handleMetricsUnauthenticated(core *vault.Core) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
req := &logical.Request{Headers: r.Header}
format := r.Form.Get("format")
if format == "" {
format = metricsutil.FormatFromRequest(req)
}

// Define response
resp, err := core.MetricsHelper().ResponseForFormat(format)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
return
}

// Manually extract the logical response and send back the information
w.WriteHeader(resp.Data[logical.HTTPStatusCode].(int))
w.Header().Set("Content-Type", resp.Data[logical.HTTPContentType].(string))
w.Write(resp.Data[logical.HTTPRawBody].([]byte))
})
}
50 changes: 50 additions & 0 deletions http/sys_metrics_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package http

import (
"testing"
"time"

"github.com/armon/go-metrics"
"github.com/hashicorp/vault/helper/metricsutil"
"github.com/hashicorp/vault/vault"
)

func TestSysMetricsUnauthenticated(t *testing.T) {
inm := metrics.NewInmemSink(10*time.Second, time.Minute)
metrics.DefaultInmemSignal(inm)
conf := &vault.CoreConfig{
BuiltinRegistry: vault.NewMockBuiltinRegistry(),
MetricsHelper: metricsutil.NewMetricsHelper(inm, false),
}
core, _, token := vault.TestCoreUnsealedWithConfig(t, conf)
ln, addr := TestServer(t, core)
TestServerAuth(t, addr, token)

// Default: Only authenticated access
resp := testHttpGet(t, "", addr+"/v1/sys/metrics")
testResponseStatus(t, resp, 400)
resp = testHttpGet(t, token, addr+"/v1/sys/metrics")
testResponseStatus(t, resp, 200)

// Close listener
ln.Close()

// Setup new custom listener with unauthenticated metrics access
ln, addr = TestListener(t)
props := &vault.HandlerProperties{
Core: core,
MaxRequestSize: DefaultMaxRequestSize,
UnauthenticatedMetricsAccess: true,
}
TestServerWithListenerAndProperties(t, ln, addr, core, props)
defer ln.Close()
TestServerAuth(t, addr, token)

// Test without token
resp = testHttpGet(t, "", addr+"/v1/sys/metrics")
testResponseStatus(t, resp, 200)

// Should also work with token
resp = testHttpGet(t, token, addr+"/v1/sys/metrics")
testResponseStatus(t, resp, 200)
}
1 change: 1 addition & 0 deletions sdk/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ require (
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 // indirect
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e
golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db // indirect
google.golang.org/appengine v1.4.0 // indirect
google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107 // indirect
google.golang.org/grpc v1.22.0
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect
Expand Down
5 changes: 3 additions & 2 deletions ui/app/adapters/secret-v2-version.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { get } from '@ember/object';
import ApplicationAdapter from './application';
import DS from 'ember-data';
import { encodePath } from 'vault/utils/path-encoding-helpers';
import ControlGroupError from 'vault/lib/control-group-error';

export default ApplicationAdapter.extend({
namespace: 'v1',
Expand All @@ -27,8 +28,8 @@ export default ApplicationAdapter.extend({

findRecord() {
return this._super(...arguments).catch(errorOrModel => {
// if it's a real 404, this will be an error, if not
// it will be the body of a deleted / destroyed version
// if the response is a real 404 or if the secret is gated by a control group this will be an error,
// otherwise the response will be the body of a deleted / destroyed version
if (errorOrModel instanceof DS.AdapterError) {
throw errorOrModel;
}
Expand Down
4 changes: 2 additions & 2 deletions ui/app/lib/control-group-error.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import EmberError from '@ember/error';
import DS from 'ember-data';

export default class ControlGroupError extends EmberError {
export default class ControlGroupError extends DS.AdapterError {
constructor(wrapInfo) {
let { accessor, creation_path, creation_time, token, ttl } = wrapInfo;
super();
Expand Down
Loading

0 comments on commit 219d6ee

Please sign in to comment.