diff --git a/README.md b/README.md index 13252a1..9f415fc 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,22 @@ Create a new job: - URL: use the hubauth-int URL: `/cron` +## Enabling Biscuit + +To use biscuit tokens instead of bearers, configure the following: + +### In Security > Secret manager + +Create new secret +- HUBAUTH_BISCUIT_ROOT_PRIVKEY: a base64 encoded p256 EC private key + +### In variables + +Add a new variable +- TOKEN_TYPE: `Biscuit` +- BISCUIT_ROOT_PRIVKEY: set to the resource ID from `HUBAUTH_BISCUIT_ROOT_PRIVKEY` + + ## Hubauth CLI Configure gcloud auth application-default with the following command, and follow the browser instructions: diff --git a/cloudbuild.yaml b/cloudbuild.yaml index fadd8a9..b779d5b 100644 --- a/cloudbuild.yaml +++ b/cloudbuild.yaml @@ -17,6 +17,8 @@ steps: '--image', 'gcr.io/$PROJECT_ID/$_APP:$BUILD_ID', '--region', '$_DEPLOY_REGION_PRIMARY', '--update-env-vars', 'BUILD_REPO=$REPO_NAME,BUILD_REV=$COMMIT_SHA', + '--command', '/app/hubauth-ext', + '--allow-unauthenticated' ] - name: 'gcr.io/google.com/cloudsdktool/cloud-sdk:slim' waitFor: ['build'] @@ -27,6 +29,8 @@ steps: '--image', 'gcr.io/$PROJECT_ID/$_APP:$BUILD_ID', '--region', '$_DEPLOY_REGION_FALLBACK', '--update-env-vars', 'BUILD_REPO=$REPO_NAME,BUILD_REV=$COMMIT_SHA', + '--command', '/app/hubauth-ext', + '--allow-unauthenticated' ] - name: 'gcr.io/google.com/cloudsdktool/cloud-sdk:slim' waitFor: ['build'] @@ -37,6 +41,7 @@ steps: '--image', 'gcr.io/$PROJECT_ID/$_APP:$BUILD_ID', '--region', '$_DEPLOY_REGION_PRIMARY', '--update-env-vars', 'BUILD_REPO=$REPO_NAME,BUILD_REV=$COMMIT_SHA', + '--command', '/app/hubauth-int', ] substitutions: _APP: hubauth diff --git a/cmd/hubauth-ext/main.go b/cmd/hubauth-ext/main.go index d78b7c7..f46890b 100644 --- a/cmd/hubauth-ext/main.go +++ b/cmd/hubauth-ext/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "encoding/json" "fmt" "log" "net/http" @@ -15,6 +16,7 @@ import ( "github.com/flynn/hubauth/pkg/datastore" "github.com/flynn/hubauth/pkg/httpapi" "github.com/flynn/hubauth/pkg/idp" + "github.com/flynn/hubauth/pkg/idp/token" "github.com/flynn/hubauth/pkg/kmssign" "github.com/flynn/hubauth/pkg/rp/google" "go.opencensus.io/plugin/ochttp" @@ -65,27 +67,68 @@ func main() { if err != nil { log.Fatalf("failed to access secret version for %s: %s", name, err) } - return result.Payload.String() + + // Payload.String() would return a json encoded version of the secret: {"data": "..."} + // the actual secret is in Data. + return string(result.Payload.Data) + } + + forcedAudienceKeyVersions := new(kmssign.ForcedAudiencesKeyVersion) + // AUDIENCE_KEYS is a env variable containing a serialized json object, holding tuples of audienceURL: keyVersion + // it allows to specify a different key to use for some audience. + // example: + // { + // "https://audience.url": "projects/PROJECT/locations/KMS_LOCATION/keyRings/KMS_KEYRING/cryptoKeys/AUDIENCE_NAME/cryptoKeyVersions/VERSION", + // "https://another.audience.url": "projects/PROJECT/locations/KMS_LOCATION/keyRings/KMS_KEYRING/cryptoKeys/AUDIENCE_NAME/cryptoKeyVersions/VERSION" + // } + if keys := os.Getenv("AUDIENCE_KEYS"); keys != "" { + if err := json.Unmarshal([]byte(keys), forcedAudienceKeyVersions); err != nil { + log.Fatalf("invalid audience keys: %v", err) + } + } + + audienceKeyNamer := kmssign.AudienceKeyNameFunc(*forcedAudienceKeyVersions, os.Getenv("PROJECT_ID"), os.Getenv("KMS_LOCATION"), os.Getenv("KMS_KEYRING")) + + var accessTokenBuilder token.AccessTokenBuilder + var rootPubKey []byte + tokenType, exists := os.LookupEnv("TOKEN_TYPE") + if !exists { + tokenType = "Bearer" + } + switch tokenType { + case "Bearer": + accessTokenBuilder = token.NewBearerBuilder(kmsClient, audienceKeyNamer) + case "Biscuit": + biscuitKey, err := token.DecodeB64PrivateKey(secret("BISCUIT_ROOT_PRIVKEY")) + if err != nil { + log.Fatalf("failed to initialize biscuit keypair: %v", err) + } + + rootPubKey = biscuitKey.Public().Bytes() + accessTokenBuilder = token.NewBiscuitBuilder(kmsClient, audienceKeyNamer, biscuitKey) + default: + log.Fatalf("invalid TOKEN_TYPE, must be one of: Bearer, Biscuit") } log.Fatal(http.ListenAndServe(":"+httpPort, &ochttp.Handler{ Propagation: &propagation.HTTPFormat{}, Handler: httpapi.New(httpapi.Config{ - IdP: idp.New(datastore.New(dsClient), + IdP: idp.New( + datastore.New(dsClient), google.New( os.Getenv("RP_GOOGLE_CLIENT_ID"), os.Getenv("RP_GOOGLE_CLIENT_SECRET"), os.Getenv("BASE_URL")+"/rp/google", ), - kmsClient, []byte(secret("CODE_KEY_SECRET")), refreshKey, - idp.AudienceKeyNameFunc(os.Getenv("PROJECT_ID"), os.Getenv("KMS_LOCATION"), os.Getenv("KMS_KEYRING")), + accessTokenBuilder, ), CookieKey: []byte(secret("COOKIE_KEY_SECRET")), ProjectID: os.Getenv("PROJECT_ID"), Repository: fmt.Sprintf("https://source.developers.google.com/p/%s/r/%s", os.Getenv("PROJECT_ID"), os.Getenv("BUILD_REPO")), Revision: os.Getenv("BUILD_REV"), + PublicKey: rootPubKey, }), }, )) diff --git a/go.mod b/go.mod index 20f3edd..09be740 100644 --- a/go.mod +++ b/go.mod @@ -9,13 +9,14 @@ require ( github.com/alecthomas/kong v0.2.12 github.com/aws/aws-sdk-go v1.34.6 // indirect github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect + github.com/flynn/biscuit-go v0.0.0-20201015081742-15d7d351f345 github.com/golang/protobuf v1.4.3 github.com/googleapis/gax-go/v2 v2.0.5 github.com/jedib0t/go-pretty/v6 v6.0.5 github.com/stretchr/testify v1.6.1 go.opencensus.io v0.22.5 go.uber.org/zap v1.16.0 - golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de + golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 golang.org/x/exp/errors v0.0.0-20200513190911-00229845015e golang.org/x/net v0.0.0-20201209123823-ac852fbbde11 golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5 diff --git a/go.sum b/go.sum index 45c0169..45ab81b 100644 --- a/go.sum +++ b/go.sum @@ -19,9 +19,8 @@ cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZ cloud.google.com/go v0.62.0 h1:RmDygqvj27Zf3fCQjQRtLyC7KwFcHkeJitcO0OoGOcA= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.68.0 h1:AnVkaPGAuWaIY/8a75HlNzZNrHDee6YL4rWkwS+CeyE= -cloud.google.com/go v0.68.0/go.mod h1:91NO4SCDjUfe1zeC0f4/dpckkUNpuNEyqm4X2KLrzNQ= cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0 h1:kpgPA77kSSbjSs+fWHkPTxQ6J5Z2Qkruo5jfXEkHxNQ= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= @@ -53,14 +52,16 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/alecthomas/kong v0.2.11 h1:RKeJXXWfg9N47RYfMm0+igkxBCTF4bzbneAxaqid0c4= -github.com/alecthomas/kong v0.2.11/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= +github.com/alecthomas/kong v0.2.12 h1:X3kkCOXGUNzLmiu+nQtoxWqj4U2a39MpSJR3QdQXOwI= github.com/alecthomas/kong v0.2.12/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= +github.com/alecthomas/participle v0.6.0/go.mod h1:HfdmEuwvr12HXQN44HPWXR0lHmVolVYe4dyL6lQ3duY= +github.com/alecthomas/participle v0.6.0/go.mod h1:HfdmEuwvr12HXQN44HPWXR0lHmVolVYe4dyL6lQ3duY= +github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= +github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= github.com/aws/aws-sdk-go v1.23.20 h1:2CBuL21P0yKdZN5urf2NxKa1ha8fhnY+A3pBCHFeZoA= github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.34.6 h1:2aPXQGkR6xeheN5dns13mSoDWeUlj4wDmfZ+8ZDHauw= github.com/aws/aws-sdk-go v1.34.6/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= -github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0 h1:t/LhUZLVitR1Ow2YOnduCsavhwFUklBMoGVYUCqmCqk= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -70,7 +71,6 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 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= @@ -79,11 +79,12 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/flynn/biscuit-go v0.0.0-20201015081742-15d7d351f345 h1:ME6bm5dwn9V2DUlfXJqeN121B5nM7rDFqLFOATALqYE= +github.com/flynn/biscuit-go v0.0.0-20201015081742-15d7d351f345/go.mod h1:Sj4oR2hNkrZH1cf3Cj5DPHc3Xq0o61GWeau6UkZR+3c= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -96,23 +97,18 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0 h1:oOuy+ugB+P/kBdUnG5QaMXSIyJ1q38wWSojYCb3z5VQ= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= @@ -121,18 +117,13 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1 h1:/exdXoGamhu5ONeUJH0deniYLWYvQwW66yvlfiiKTu0= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -143,7 +134,6 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200905233945-acf8798be1f7/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= @@ -151,13 +141,14 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gtank/ristretto255 v0.1.2 h1:JEqUCPA1NvLq5DwYtuzigd7ss8fwbYay9fi4/5uMzcc= +github.com/gtank/ristretto255 v0.1.2/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/jedib0t/go-pretty/v6 v6.0.5 h1:oOo0/jSb3NEYKT6l1hhFXoX2UZnkanMuCE2DVT1mqnE= github.com/jedib0t/go-pretty/v6 v6.0.5/go.mod h1:MTr6FgcfNdnN5wPVBzJ6mhJeDyiF0yBvS2TMXEV/XSU= -github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc= github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= @@ -172,7 +163,6 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 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= @@ -186,7 +176,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ 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.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -197,9 +186,7 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4 h1:LYy1Hy3MJdrCdMwwzxA/dRok4ejH+RwNGbuoD9fCjto= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= @@ -214,11 +201,10 @@ go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= 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-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig= -golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 h1:hb9wdF1z5waM+dSIICn1l0DkLVDT3hqhhQsDNUmHPRE= +golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -227,7 +213,6 @@ golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd h1:zkO/Lhoka23X63N9OSzpSeROEUQ5ODw47tM3YWjygbs= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6 h1:QE6XYQK6naiK1EPAe1g/ILLxN5RBoH5xkJk3CqlMI/Y= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= @@ -243,10 +228,9 @@ golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367 h1:0IiAsCRByjO2QjX7ZPkw5oU9x+n1YqRL802rjC0c3Aw= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 h1:2M3HP5CCK1Si9FQhwnzYhXdG6DXeebvUHFpre8QvbyI= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= @@ -254,14 +238,12 @@ golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKG golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0 h1:8pl+sMODzuvGJkmj2W4kZihvVb5mKm8pB/X44PIQHv8= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -275,49 +257,40 @@ golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0 h1:MsuvTghUPjX762sGLnGsxC3HM0B5r83wEtYcYR8/vRs= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200927032502-5d4f70055728/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201010224723-4f7140c49acb h1:mUVeFHoDKis5nxCAzoAi7E8Ghb86EXh/RK6wtvJIqRY= -golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11 h1:lwlPPsmjDKK0J6eG6xDWd5XPehI0R024zxjDnw3esPA= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 h1:ld7aEMNHoBnnDAX15v1T6z31v8HwR2A9FYOuAhWqkwc= golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5 h1:Lm4OryKCca1vehdsWogr9N4t7NfZxLbJoc/H0w4K4S4= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180816055513-1c9583448a9c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -335,9 +308,7 @@ golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4 h1:sfkvUWPNGwSV+8/fNqctR5lS2AqCSqYwXdrjCxp/dXo= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -346,26 +317,22 @@ golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642 h1:B6caxRw+hozq68X2MY7jEpZh/cr4/aHLv9xU8Kkadrw= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f h1:Fqb3ao1hUmOR3GkUOg/Y+BadLwykBIzs5q8Ez2SbHyc= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3 h1:kzM6+9dur93BcC2kVlYl34cHU+TYZLanmpSJHVMmL64= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -397,7 +364,6 @@ golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56 h1:DFtSed2q3HtNuVazwVDZ4nSRS/JrZEig0gz2BY4VNrg= golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= @@ -408,21 +374,17 @@ golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7 h1:LHW24ah7B+uV/OePwNP0p/t889F3QSyLvY8Sg/bK0SY= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d h1:szSOL78iTCl0LF1AMjhSWJj8tIM0KixlUUnBtYXsmd8= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= golang.org/x/tools v0.0.0-20200916150407-587cf2330ce8/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= -golang.org/x/tools v0.0.0-20201002184944-ecd9fd270d5d h1:vWQvJ/Z0Lu+9/8oQ/pAYXNzbc7CMnBl+tULGVHOy3oE= -golang.org/x/tools v0.0.0-20201002184944-ecd9fd270d5d/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2 h1:vEtypaVub6UvKkiXZ2xx9QIvp9TL7sI7xp7vdi2kezA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 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 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 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= @@ -434,36 +396,27 @@ google.golang.org/api v0.10.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhE google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0 h1:0q95w+VuFtv4PAx4PZVQdBMmYbaCHbnfKaEiDIcVyag= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0 h1:TgDr+1inK2XVUKZx3BYAqQg/GwucGdBkzZjWaTg/I+A= google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0 h1:jMF5hhVfMkTZwHW1SDpKq5CkgWLXOb31Foaca9Zr3oM= google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0 h1:BaiDisFir8O4IJxvAabCGGkQ6yCJegNQqSVoYUNAnbk= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0 h1:yfrXXP61wVuLb0vBcG6qaOoIoqYEzOQS8jum51jkv2w= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.32.0 h1:Le77IccnTqEa8ryp9wIpX5W3zYm7Gf9LhOp9PHcwFts= google.golang.org/api v0.32.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.33.0 h1:+gL0XvACeMIvpwLZ5rQZzLn5cwOsgg8dIcfJ2SYfBVw= -google.golang.org/api v0.33.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0 h1:l2Nfbl2GPXdWorv+dT2XfinX2jOOw4zv1VhLstx+6rE= google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.2/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -480,9 +433,7 @@ google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvx google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce h1:1mbrb1tUU+Zmt5C94IGKADBTJZjZXAd+BubWi7r9EiI= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63 h1:YzfoEYWbODU5Fbt37+h7X16BWQbad7Q4S6gclTKFXM8= google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= @@ -491,22 +442,16 @@ google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790 h1:FGjyjrQGURdc98leD1P65IdQD9Zlr4McvRcqIlV6OSs= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f h1:ohwtWcCwB/fZUxh/vjazHorYmBnua3NmY3CAjwC7mEA= google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c h1:Lq4llNryJoaVFRmvrIwC/ZHH7tNt4tUYIu8+se2aayY= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200916143405-f6a2fa72f0c4/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201002142447-3860012362da/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201014134559-03b6142f0dc9 h1:fG84H9C3EXfuDlzkG+VEPDYHHExklP6scH1QZ5gQTqU= -google.golang.org/genproto v0.0.0-20201014134559-03b6142f0dc9/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc h1:BgQmMjmd7K1zov8j8lYULHW0WnmBGUIMp6+VDwlGErc= google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= @@ -516,34 +461,24 @@ google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1 h1:zvIju4sqAGvwKspUQOhwnpcqSbzi7/H6QomNNjTL4sk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0 h1:bO/TA4OxCOummhSf10siHuG7vJOiwh7SpRpFZDkOgl4= google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0 h1:M5a8xTlYTxwMn5ZFkwhRabsygDY5G8TYLyQDBxJNAxE= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0 h1:T7P4R73V3SSDPhH7WW7ATbfViLtmamH0DKrP3f9AuDI= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1 h1:SfXqXS5hkufcdZ/mHtYCh53P2b+92WQq/DZcKLgsFRs= google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.32.0 h1:zWTV+LMdc3kaiJMSTOFz2UgSBgx8RNQoTGiZu3fR9S0= google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0 h1:raiipEjMOIC/TO2AvyTxP25XFdLxNIBwzDh3FM3XztI= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0 h1:qdOKuR/EIArgaWNjetjgTzgVTAZ+S/WXVrq9HW9zimw= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= @@ -559,9 +494,7 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3 h1:sXmLre5bzIR6ypkjXCDI3jHPssRhc8KD/Ome589sc3U= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= diff --git a/pkg/cli/audiences.go b/pkg/cli/audiences.go index 172b62d..fbe2256 100644 --- a/pkg/cli/audiences.go +++ b/pkg/cli/audiences.go @@ -12,19 +12,25 @@ import ( "github.com/flynn/hubauth/pkg/hubauth" "github.com/jedib0t/go-pretty/v6/table" "google.golang.org/genproto/googleapis/cloud/kms/v1" + kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1" + fieldmask "google.golang.org/genproto/protobuf/field_mask" ) type audiencesCmd struct { - List audiencesListCmd `kong:"cmd,help='list audiences',default:'1'"` - Create audiencesCreateCmd `kong:"cmd,help='create audience'"` - UpdateType audienceUpdateTypeCmd `kong:"cmd,name='update-type',help='change audience type'"` - UpdateClientIDs audiencesUpdateClientsIDsCmd `kong:"cmd,name='update-client-ids',help='add or remove audience client IDs'"` - Delete audiencesDeleteCmd `kong:"cmd,help='delete audience and all its keys'"` - ListPolicies audiencesListPoliciesCmd `kong:"cmd,name='list-policies',help='list audience policies'"` - SetPolicy audiencesSetPolicyCmd `kong:"cmd,name='set-policy',help='set audience auth policy'"` - UpdatePolicy audiencesUpdatePolicyCmd `kong:"cmd,name='update-policy',help='modify audience policy api user or groups'"` - DeletePolicy audiencesDeletePolicyCmd `kong:"cmd,name='delete-policy',help='delete audience auth policy'"` - Key audiencesKeyCmd `kong:"cmd,help='get audience public key'"` + List audiencesListCmd `kong:"cmd,help='list audiences',default:'1'"` + Create audiencesCreateCmd `kong:"cmd,help='create audience'"` + UpdateType audienceUpdateTypeCmd `kong:"cmd,name='update-type',help='change audience type'"` + UpdateClientIDs audiencesUpdateClientsIDsCmd `kong:"cmd,name='update-client-ids',help='add or remove audience client IDs'"` + Delete audiencesDeleteCmd `kong:"cmd,help='delete audience and all its keys'"` + ListPolicies audiencesListPoliciesCmd `kong:"cmd,name='list-policies',help='list audience policies'"` + SetPolicy audiencesSetPolicyCmd `kong:"cmd,name='set-policy',help='set audience auth policy'"` + UpdatePolicy audiencesUpdatePolicyCmd `kong:"cmd,name='update-policy',help='modify audience policy api user or groups'"` + DeletePolicy audiencesDeletePolicyCmd `kong:"cmd,name='delete-policy',help='delete audience auth policy'"` + Key audiencesKeyCmd `kong:"cmd,help='get audience public key'"` + ListKeyVersions audiencesListKeyVersionsCmd `kong:"cmd,help='list audience key versions'"` + CreateKeyVersion audiencesCreateKeyVersionCmd `kong:"cmd,help='create a new audience key version'"` + DeleteKeyVersion audiencesDeleteKeyVersionCmd `kong:"cmd,help='schedule an audience key version for deletion'"` + RestoreKeyVersion audiencesRestoreKeyVersionCmd `kong:"cmd,help='restore an audience key version scheduled for deletion'"` } type audiencesListCmd struct{} @@ -102,7 +108,7 @@ type audienceUpdateTypeCmd struct { func (c *audienceUpdateTypeCmd) Run(cfg *Config) error { return cfg.DB.MutateAudience(context.Background(), c.AudienceURL, []*hubauth.AudienceMutation{{ - Op: hubauth.AudienceMutationSetType, + Op: hubauth.AudienceMutationOpSetType, Type: c.AudienceType, }}) @@ -139,18 +145,13 @@ type audiencesDeleteCmd struct { } func (c *audiencesDeleteCmd) Run(cfg *Config) error { - u, err := url.Parse(c.AudienceURL) + keyName, err := cryptoKeyName(cfg.ProjectID, c.KMSLocation, c.KMSKeyring, c.AudienceURL) if err != nil { - return fmt.Errorf("error parsing audience URL: %w", err) + return fmt.Errorf("invalid key name: %w", err) } versions, err := cfg.KMS.ListCryptoKeyVersions(context.Background(), &kms.ListCryptoKeyVersionsRequest{ - Parent: fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s", - cfg.ProjectID, - c.KMSLocation, - c.KMSKeyring, - strings.Replace(u.Host, ".", "_", -1), - ), + Parent: keyName, }) if err != nil { @@ -161,11 +162,15 @@ func (c *audiencesDeleteCmd) Run(cfg *Config) error { if _, err = cfg.KMS.DestroyCryptoKeyVersion(context.Background(), &kms.DestroyCryptoKeyVersionRequest{ Name: version.Name, }); err != nil { - return fmt.Errorf("failed to delete crypto key version %s: %v", version.Name, err) + return fmt.Errorf("failed to delete crypto key version %s: %w", version.Name, err) } } - return cfg.DB.DeleteAudience(context.Background(), c.AudienceURL) + if err := cfg.DB.DeleteAudience(context.Background(), c.AudienceURL); err != nil { + return fmt.Errorf("failed to delete audience: %w", err) + } + + return nil } type audiencesListPoliciesCmd struct { @@ -256,32 +261,174 @@ func (c *audiencesDeletePolicyCmd) Run(cfg *Config) error { type audiencesKeyCmd struct { URL string `kong:"required,name='audience-url',help='audience URL'"` + KeyVersion int `kong:"name='key-version',help='key version',default=1"` KMSLocation string `kong:"name='kms-location',default='us',help='KMS keyring location'"` KMSKeyring string `kong:"name='kms-keyring',default='hubauth-audiences-us',help='KMS keyring name'"` } func (c *audiencesKeyCmd) Run(cfg *Config) error { ctx := context.Background() - - u, err := url.Parse(c.URL) + keyVersion, err := cryptoKeyVersion(cfg.ProjectID, c.KMSLocation, c.KMSKeyring, c.URL, c.KeyVersion) if err != nil { - return fmt.Errorf("error parsing audience URL: %w", err) - } - if u.Scheme != "https" { - return fmt.Errorf("audience URL must be https://") - } - if u.Path != "" { - return fmt.Errorf("unexpected path in audience URL") + return fmt.Errorf("invalid key version: %w", err) } res, err := cfg.KMS.GetPublicKey(ctx, &kms.GetPublicKeyRequest{ - Name: fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s/cryptoKeyVersions/1", cfg.ProjectID, c.KMSLocation, c.KMSKeyring, strings.Replace(u.Host, ".", "_", -1)), + Name: keyVersion, }) if err != nil { - return err + return fmt.Errorf("failed to fetch public key: %w", err) } b, _ := pem.Decode([]byte(res.Pem)) fmt.Println(base64.URLEncoding.EncodeToString(b.Bytes)) return nil } + +type audiencesListKeyVersionsCmd struct { + URL string `kong:"required,name='audience-url',help='audience URL'"` + KMSLocation string `kong:"name='kms-location',default='us',help='KMS keyring location'"` + KMSKeyring string `kong:"name='kms-keyring',default='hubauth-audiences-us',help='KMS keyring name'"` +} + +func (c *audiencesListKeyVersionsCmd) Run(cfg *Config) error { + keyName, err := cryptoKeyName(cfg.ProjectID, c.KMSLocation, c.KMSKeyring, c.URL) + if err != nil { + return fmt.Errorf("invalid key name: %w", err) + } + versions, err := cfg.KMS.ListCryptoKeyVersions(context.Background(), &kms.ListCryptoKeyVersionsRequest{ + Parent: keyName, + }) + if err != nil { + return fmt.Errorf("failed to list versions: %w", err) + } + + t := table.NewWriter() + t.SetOutputMirror(os.Stdout) + t.AppendHeader(table.Row{"Version", "State", "Alg", "CreateTime", "DestroyTime"}) + for _, v := range versions { + split := strings.Split(v.Name, "/") + versionID := split[len(split)-1] + + destroyedAt := "" + if v.DestroyTime != nil { + destroyedAt = v.DestroyTime.AsTime().String() + } + + t.AppendRow(table.Row{versionID, v.State, v.Algorithm, v.CreateTime.AsTime(), destroyedAt}) + } + t.Render() + + return nil +} + +type audiencesCreateKeyVersionCmd struct { + URL string `kong:"required,name='audience-url',help='audience URL'"` + KMSLocation string `kong:"name='kms-location',default='us',help='KMS keyring location'"` + KMSKeyring string `kong:"name='kms-keyring',default='hubauth-audiences-us',help='KMS keyring name'"` +} + +func (c *audiencesCreateKeyVersionCmd) Run(cfg *Config) error { + keyName, err := cryptoKeyName(cfg.ProjectID, c.KMSLocation, c.KMSKeyring, c.URL) + if err != nil { + return fmt.Errorf("invalid key name: %w", err) + } + + v, err := cfg.KMS.CreateCryptoKeyVersion(context.Background(), &kms.CreateCryptoKeyVersionRequest{ + Parent: keyName, + }) + if err != nil { + return fmt.Errorf("error creating audience key: %w", err) + } + + fmt.Println(v.Name) + + return nil +} + +type audiencesDeleteKeyVersionCmd struct { + URL string `kong:"required,name='audience-url',help='audience URL'"` + KeyVersion int `kong:"required,name='key-version',help='key version'"` + KMSLocation string `kong:"name='kms-location',default='us',help='KMS keyring location'"` + KMSKeyring string `kong:"name='kms-keyring',default='hubauth-audiences-us',help='KMS keyring name'"` +} + +func (c *audiencesDeleteKeyVersionCmd) Run(cfg *Config) error { + keyVersion, err := cryptoKeyVersion(cfg.ProjectID, c.KMSLocation, c.KMSKeyring, c.URL, c.KeyVersion) + if err != nil { + return fmt.Errorf("invalid key version: %w", err) + } + + if _, err = cfg.KMS.DestroyCryptoKeyVersion(context.Background(), &kms.DestroyCryptoKeyVersionRequest{ + Name: keyVersion, + }); err != nil { + return fmt.Errorf("failed to delete crypto key version: %w", err) + } + + return nil +} + +type audiencesRestoreKeyVersionCmd struct { + URL string `kong:"required,name='audience-url',help='audience URL'"` + KeyVersion int `kong:"required,name='key-version',help='key version'"` + KMSLocation string `kong:"name='kms-location',default='us',help='KMS keyring location'"` + KMSKeyring string `kong:"name='kms-keyring',default='hubauth-audiences-us',help='KMS keyring name'"` +} + +func (c *audiencesRestoreKeyVersionCmd) Run(cfg *Config) error { + keyVersion, err := cryptoKeyVersion(cfg.ProjectID, c.KMSLocation, c.KMSKeyring, c.URL, c.KeyVersion) + if err != nil { + return fmt.Errorf("invalid key version: %w", err) + } + + key, err := cfg.KMS.RestoreCryptoKeyVersion(context.Background(), &kms.RestoreCryptoKeyVersionRequest{ + Name: keyVersion, + }) + if err != nil { + return fmt.Errorf("failed to restore key version: %w", err) + } + + // restored keys are in disabled state, so this enable it + _, err = cfg.KMS.UpdateCryptoKeyVersion(context.Background(), &kms.UpdateCryptoKeyVersionRequest{ + CryptoKeyVersion: &kmspb.CryptoKeyVersion{ + Name: key.Name, + State: kmspb.CryptoKeyVersion_ENABLED, + }, + UpdateMask: &fieldmask.FieldMask{ + Paths: []string{"state"}, + }, + }) + if err != nil { + return fmt.Errorf("failed to update key version state: %w", err) + } + + return nil +} + +func cryptoKeyName(projectID, kmsLocation, kmsKeyring string, audienceURL string) (string, error) { + u, err := url.Parse(audienceURL) + if err != nil { + return "", err + } + if u.Scheme != "https" { + return "", fmt.Errorf("audience URL must be https://") + } + if u.Path != "" { + return "", fmt.Errorf("unexpected path in audience URL") + } + + return fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s", + projectID, + kmsLocation, + kmsKeyring, + strings.Replace(u.Host, ".", "_", -1), + ), nil +} + +func cryptoKeyVersion(projectID, kmsLocation, kmsKeyring string, audienceURL string, version int) (string, error) { + name, err := cryptoKeyName(projectID, kmsLocation, kmsKeyring, audienceURL) + if err != nil { + return "", err + } + return fmt.Sprintf("%s/cryptoKeyVersions/%d", name, version), nil +} diff --git a/pkg/cli/audiences_test.go b/pkg/cli/audiences_test.go index 5da7475..0cdd813 100644 --- a/pkg/cli/audiences_test.go +++ b/pkg/cli/audiences_test.go @@ -22,6 +22,8 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/genproto/googleapis/cloud/kms/v1" kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1" + fieldmask "google.golang.org/genproto/protobuf/field_mask" + "google.golang.org/protobuf/types/known/timestamppb" ) type mockKMS struct { @@ -46,6 +48,18 @@ func (m *mockKMS) DestroyCryptoKeyVersion(ctx context.Context, req *kmspb.Destro args := m.Called(ctx, req) return args.Get(0).(*kmspb.CryptoKeyVersion), args.Error(1) } +func (m *mockKMS) CreateCryptoKeyVersion(ctx context.Context, req *kmspb.CreateCryptoKeyVersionRequest, opts ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) { + args := m.Called(ctx, req) + return args.Get(0).(*kmspb.CryptoKeyVersion), args.Error(1) +} +func (m *mockKMS) RestoreCryptoKeyVersion(ctx context.Context, req *kmspb.RestoreCryptoKeyVersionRequest, opts ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) { + args := m.Called(ctx, req) + return args.Get(0).(*kmspb.CryptoKeyVersion), args.Error(1) +} +func (m *mockKMS) UpdateCryptoKeyVersion(ctx context.Context, req *kmspb.UpdateCryptoKeyVersionRequest, opts ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) { + args := m.Called(ctx, req) + return args.Get(0).(*kmspb.CryptoKeyVersion), args.Error(1) +} type mockAudienceDatastore struct { mock.Mock @@ -326,6 +340,7 @@ func TestAudienceKeyCmd(t *testing.T) { KMSKeyring: "kmsKeyring", KMSLocation: "kmsLocation", URL: "https://audience.url", + KeyVersion: 2, } cfg := &Config{ @@ -334,7 +349,12 @@ func TestAudienceKeyCmd(t *testing.T) { ProjectID: "projectID", } - expectedKeyName := fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s/cryptoKeyVersions/1", cfg.ProjectID, cmd.KMSLocation, cmd.KMSKeyring, "audience_url") + expectedKeyVersion, err := cryptoKeyVersion(cfg.ProjectID, cmd.KMSLocation, cmd.KMSKeyring, cmd.URL, 2) + require.NoError(t, err) + + cfg.DB.(*mockAudienceDatastore).On("GetAudience", mock.Anything, cmd.URL).Return(&hubauth.Audience{ + URL: cmd.URL, + }, nil) privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) @@ -343,7 +363,7 @@ func TestAudienceKeyCmd(t *testing.T) { pubKeyPEM := string(pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubKeyDER})) expectedPublicKey := &kmspb.PublicKey{Pem: pubKeyPEM} - cfg.KMS.(*mockKMS).On("GetPublicKey", mock.Anything, &kmspb.GetPublicKeyRequest{Name: expectedKeyName}).Return(expectedPublicKey, nil) + cfg.KMS.(*mockKMS).On("GetPublicKey", mock.Anything, &kmspb.GetPublicKeyRequest{Name: expectedKeyVersion}).Return(expectedPublicKey, nil) r, w, err := os.Pipe() require.NoError(t, err) @@ -406,7 +426,7 @@ func TestAudienceKeyErrors(t *testing.T) { err := cmd.Run(cfg) if testCase.ExpectedErr != nil { - require.Equal(t, testCase.ExpectedErr, err) + require.Equal(t, testCase.ExpectedErr, errors.Unwrap(err)) } else { require.Error(t, err) } @@ -484,12 +504,11 @@ func TestAudienceDeleteCmd(t *testing.T) { versions := []*kmspb.CryptoKeyVersion{{Name: "v1"}, {Name: "v2"}} + expectedKeyName, err := cryptoKeyName(cfg.ProjectID, cmd.KMSLocation, cmd.KMSKeyring, cmd.AudienceURL) + require.NoError(t, err) + cfg.KMS.(*mockKMS).On("ListCryptoKeyVersions", mock.Anything, &kmspb.ListCryptoKeyVersionsRequest{ - Parent: fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/cryptoKeys/removed_audience_url", - cfg.ProjectID, - cmd.KMSLocation, - cmd.KMSKeyring, - ), + Parent: expectedKeyName, }).Return(versions, nil) cfg.KMS.(*mockKMS).On("DestroyCryptoKeyVersion", mock.Anything, &kms.DestroyCryptoKeyVersionRequest{Name: "v1"}).Once().Return(&kmspb.CryptoKeyVersion{}, nil) @@ -499,6 +518,67 @@ func TestAudienceDeleteCmd(t *testing.T) { require.NoError(t, cmd.Run(cfg)) } +func TestAudienceDeleteErrors(t *testing.T) { + testCases := []struct { + Desc string + AudienceURL string + ListCryptoKeyVersionsErr error + DestroyCryptoKeyVersionErr error + DeleteAudienceErr error + ExpectedErr error + }{ + { + Desc: "audience url fail to parse", + AudienceURL: "://audience.url", + }, + { + Desc: "fail to list keys", + AudienceURL: "https://audience.url", + ListCryptoKeyVersionsErr: errors.New("list key versions error"), + ExpectedErr: errors.New("list key versions error"), + }, + { + Desc: "fail to destroy key", + AudienceURL: "https://audience.url", + DestroyCryptoKeyVersionErr: errors.New("destroy key error"), + ExpectedErr: errors.New("destroy key error"), + }, + { + Desc: "fail to delete audience", + AudienceURL: "https://audience.url", + DeleteAudienceErr: errors.New("delete audience error"), + ExpectedErr: errors.New("delete audience error"), + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Desc, func(t *testing.T) { + cmd := &audiencesDeleteCmd{ + AudienceURL: testCase.AudienceURL, + KMSLocation: "global", + KMSKeyring: "keyring", + } + + cfg := &Config{ + DB: &mockAudienceDatastore{}, + KMS: &mockKMS{}, + ProjectID: "projectID", + } + + cfg.KMS.(*mockKMS).On("ListCryptoKeyVersions", mock.Anything, mock.Anything).Return([]*kmspb.CryptoKeyVersion{{Name: "v1"}}, testCase.ListCryptoKeyVersionsErr) + cfg.KMS.(*mockKMS).On("DestroyCryptoKeyVersion", mock.Anything, mock.Anything).Return(&kmspb.CryptoKeyVersion{}, testCase.DestroyCryptoKeyVersionErr) + + cfg.DB.(*mockAudienceDatastore).On("DeleteAudience", mock.Anything, cmd.AudienceURL).Return(testCase.DeleteAudienceErr) + err := cmd.Run(cfg) + if testCase.ExpectedErr != nil { + require.Equal(t, testCase.ExpectedErr, errors.Unwrap(err)) + } else { + require.Error(t, err) + } + }) + } +} + func TestAudienceListPolicies(t *testing.T) { cmd := &audiencesListPoliciesCmd{ AudienceURL: "https://audience.url", @@ -615,7 +695,7 @@ func TestAudienceUpdateTypeCmd(t *testing.T) { DB: &mockAudienceDatastore{}, } muts := []*hubauth.AudienceMutation{{ - Op: hubauth.AudienceMutationSetType, + Op: hubauth.AudienceMutationOpSetType, Type: cmd.AudienceType, }} @@ -623,3 +703,348 @@ func TestAudienceUpdateTypeCmd(t *testing.T) { require.NoError(t, cmd.Run(cfg)) } + +func TestAudiencesListKeyVersionsCmd(t *testing.T) { + cmd := &audiencesListKeyVersionsCmd{ + URL: "https://audience.url", + KMSKeyring: "keyring", + KMSLocation: "location", + } + + cfg := &Config{ + KMS: &mockKMS{}, + ProjectID: "project-id", + } + + keyName, err := cryptoKeyName(cfg.ProjectID, cmd.KMSLocation, cmd.KMSKeyring, cmd.URL) + require.NoError(t, err) + + now := time.Now() + versions := []*kmspb.CryptoKeyVersion{ + { + Name: "resource/name/1", + State: kms.CryptoKeyVersion_ENABLED, + Algorithm: kms.CryptoKeyVersion_EC_SIGN_P256_SHA256, + CreateTime: timestamppb.New(now.Add(-5 * time.Minute)), + }, + { + Name: "resource/name/2", + State: kms.CryptoKeyVersion_DESTROY_SCHEDULED, + Algorithm: kms.CryptoKeyVersion_EC_SIGN_P384_SHA384, + CreateTime: timestamppb.New(now.Add(-10 * time.Second)), + DestroyTime: timestamppb.New(now), + }, + } + + cfg.KMS.(*mockKMS).On("ListCryptoKeyVersions", mock.Anything, &kms.ListCryptoKeyVersionsRequest{ + Parent: keyName, + }).Return(versions, nil) + + r, w, err := os.Pipe() + require.NoError(t, err) + origStdout := os.Stdout + os.Stdout = w + + require.NoError(t, cmd.Run(cfg)) + + os.Stdout = origStdout + buf := make([]byte, 2048) + n, err := r.Read(buf) + require.NoError(t, err) + + expectedBuf := new(bytes.Buffer) + tw := table.NewWriter() + tw.SetOutputMirror(expectedBuf) + tw.AppendHeader(table.Row{"Version", "State", "Alg", "CreateTime", "DestroyTime"}) + tw.AppendRow(table.Row{"1", kmspb.CryptoKeyVersion_ENABLED, kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256, timestamppb.New(now.Add(-5 * time.Minute)).AsTime(), ""}) + tw.AppendRow(table.Row{"2", kmspb.CryptoKeyVersion_DESTROY_SCHEDULED, kmspb.CryptoKeyVersion_EC_SIGN_P384_SHA384, timestamppb.New(now.Add(-10 * time.Second)).AsTime(), timestamppb.New(now).AsTime()}) + tw.Render() + + require.Equal(t, expectedBuf.String(), string(buf[:n])) +} + +func TestAudiencesListKeyVersionsErrors(t *testing.T) { + testCases := []struct { + Desc string + AudienceURL string + ListCryptoKeyVersionsErr error + ExpectedErr error + }{ + { + Desc: "audience url fail to parse", + AudienceURL: "://audience.url", + }, + { + Desc: "fail to list keys", + AudienceURL: "https://audience.url", + ListCryptoKeyVersionsErr: errors.New("list key versions error"), + ExpectedErr: errors.New("list key versions error"), + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Desc, func(t *testing.T) { + cmd := &audiencesListKeyVersionsCmd{ + URL: testCase.AudienceURL, + KMSLocation: "global", + KMSKeyring: "keyring", + } + + cfg := &Config{ + KMS: &mockKMS{}, + ProjectID: "projectID", + } + + cfg.KMS.(*mockKMS).On("ListCryptoKeyVersions", mock.Anything, mock.Anything).Return([]*kmspb.CryptoKeyVersion{{Name: "v1"}}, testCase.ListCryptoKeyVersionsErr) + err := cmd.Run(cfg) + if testCase.ExpectedErr != nil { + require.Equal(t, testCase.ExpectedErr, errors.Unwrap(err)) + } else { + require.Error(t, err) + } + }) + } +} + +func TestAudiencesCreateKeyVersionCmd(t *testing.T) { + cmd := &audiencesCreateKeyVersionCmd{ + URL: "https://audience.url", + KMSKeyring: "keyring", + KMSLocation: "location", + } + + cfg := &Config{ + KMS: &mockKMS{}, + ProjectID: "project-id", + } + + keyName, err := cryptoKeyName(cfg.ProjectID, cmd.KMSLocation, cmd.KMSKeyring, cmd.URL) + require.NoError(t, err) + + now := time.Now() + version := &kmspb.CryptoKeyVersion{ + Name: "resource/name/5", + State: kms.CryptoKeyVersion_ENABLED, + Algorithm: kms.CryptoKeyVersion_EC_SIGN_P256_SHA256, + CreateTime: timestamppb.New(now.Add(-5 * time.Minute)), + } + + cfg.KMS.(*mockKMS).On("CreateCryptoKeyVersion", mock.Anything, &kms.CreateCryptoKeyVersionRequest{ + Parent: keyName, + }).Return(version, nil) + + r, w, err := os.Pipe() + require.NoError(t, err) + origStdout := os.Stdout + os.Stdout = w + + require.NoError(t, cmd.Run(cfg)) + + os.Stdout = origStdout + buf := make([]byte, 2048) + n, err := r.Read(buf) + require.NoError(t, err) + require.Equal(t, version.Name+"\n", string(buf[:n])) +} + +func TestAudiencesCreateKeyVersionErrors(t *testing.T) { + testCases := []struct { + Desc string + AudienceURL string + CreateCryptoKeyVersionErr error + ExpectedErr error + }{ + { + Desc: "audience url fail to parse", + AudienceURL: "://audience.url", + }, + { + Desc: "fail to create key", + AudienceURL: "https://audience.url", + CreateCryptoKeyVersionErr: errors.New("create key versions error"), + ExpectedErr: errors.New("create key versions error"), + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Desc, func(t *testing.T) { + cmd := &audiencesCreateKeyVersionCmd{ + URL: testCase.AudienceURL, + KMSLocation: "global", + KMSKeyring: "keyring", + } + + cfg := &Config{ + KMS: &mockKMS{}, + ProjectID: "projectID", + } + + cfg.KMS.(*mockKMS).On("CreateCryptoKeyVersion", mock.Anything, mock.Anything).Return(&kmspb.CryptoKeyVersion{}, testCase.CreateCryptoKeyVersionErr) + + err := cmd.Run(cfg) + if testCase.ExpectedErr != nil { + require.Equal(t, testCase.ExpectedErr, errors.Unwrap(err)) + } else { + require.Error(t, err) + } + }) + } +} + +func TestAudiencesDeleteKeyVersionCmd(t *testing.T) { + cmd := &audiencesDeleteKeyVersionCmd{ + URL: "https://audience.url", + KeyVersion: 1, + KMSKeyring: "keyring", + KMSLocation: "location", + } + + cfg := &Config{ + KMS: &mockKMS{}, + ProjectID: "project-id", + } + + keyVersion, err := cryptoKeyVersion(cfg.ProjectID, cmd.KMSLocation, cmd.KMSKeyring, cmd.URL, cmd.KeyVersion) + require.NoError(t, err) + + cfg.KMS.(*mockKMS).On("DestroyCryptoKeyVersion", mock.Anything, &kms.DestroyCryptoKeyVersionRequest{ + Name: keyVersion, + }).Return(&kmspb.CryptoKeyVersion{}, nil) + + require.NoError(t, cmd.Run(cfg)) +} + +func TestAudiencesDeleteKeyVersionErrors(t *testing.T) { + testCases := []struct { + Desc string + AudienceURL string + DeleteCryptoKeyVersionErr error + ExpectedErr error + }{ + { + Desc: "audience url fail to parse", + AudienceURL: "://audience.url", + }, + { + Desc: "fail to delete key", + AudienceURL: "https://audience.url", + DeleteCryptoKeyVersionErr: errors.New("delete key versions error"), + ExpectedErr: errors.New("delete key versions error"), + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Desc, func(t *testing.T) { + cmd := &audiencesDeleteKeyVersionCmd{ + URL: testCase.AudienceURL, + KeyVersion: 12, + KMSLocation: "global", + KMSKeyring: "keyring", + } + + cfg := &Config{ + KMS: &mockKMS{}, + ProjectID: "projectID", + } + + cfg.KMS.(*mockKMS).On("DestroyCryptoKeyVersion", mock.Anything, mock.Anything).Return(&kmspb.CryptoKeyVersion{}, testCase.DeleteCryptoKeyVersionErr) + + err := cmd.Run(cfg) + if testCase.ExpectedErr != nil { + require.Equal(t, testCase.ExpectedErr, errors.Unwrap(err)) + } else { + require.Error(t, err) + } + }) + } +} + +func TestAudiencesRestoreKeyVersionCmd(t *testing.T) { + cmd := &audiencesRestoreKeyVersionCmd{ + URL: "https://audience.url", + KeyVersion: 1, + KMSKeyring: "keyring", + KMSLocation: "location", + } + + cfg := &Config{ + KMS: &mockKMS{}, + ProjectID: "project-id", + } + + keyVersion, err := cryptoKeyVersion(cfg.ProjectID, cmd.KMSLocation, cmd.KMSKeyring, cmd.URL, cmd.KeyVersion) + require.NoError(t, err) + + key := &kmspb.CryptoKeyVersion{ + Name: "key123", + } + + cfg.KMS.(*mockKMS).On("RestoreCryptoKeyVersion", mock.Anything, &kms.RestoreCryptoKeyVersionRequest{ + Name: keyVersion, + }).Return(key, nil) + + cfg.KMS.(*mockKMS).On("UpdateCryptoKeyVersion", mock.Anything, &kms.UpdateCryptoKeyVersionRequest{ + CryptoKeyVersion: &kmspb.CryptoKeyVersion{ + Name: key.Name, + State: kmspb.CryptoKeyVersion_ENABLED, + }, + UpdateMask: &fieldmask.FieldMask{ + Paths: []string{"state"}, + }, + }).Return(&kmspb.CryptoKeyVersion{}, nil) + + require.NoError(t, cmd.Run(cfg)) +} + +func TestAudiencesRestoreKeyVersionErrors(t *testing.T) { + testCases := []struct { + Desc string + AudienceURL string + RestoreCryptoKeyVersionErr error + UpdateCryptoKeyVersionErr error + ExpectedErr error + }{ + { + Desc: "audience url fail to parse", + AudienceURL: "://audience.url", + }, + { + Desc: "fail to restore key", + AudienceURL: "https://audience.url", + RestoreCryptoKeyVersionErr: errors.New("restore key version error"), + ExpectedErr: errors.New("restore key version error"), + }, + { + Desc: "fail to update key", + AudienceURL: "https://audience.url", + UpdateCryptoKeyVersionErr: errors.New("update key version error"), + ExpectedErr: errors.New("update key version error"), + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Desc, func(t *testing.T) { + cmd := &audiencesRestoreKeyVersionCmd{ + URL: testCase.AudienceURL, + KeyVersion: 12, + KMSLocation: "global", + KMSKeyring: "keyring", + } + + cfg := &Config{ + KMS: &mockKMS{}, + ProjectID: "projectID", + } + + cfg.KMS.(*mockKMS).On("RestoreCryptoKeyVersion", mock.Anything, mock.Anything).Return(&kmspb.CryptoKeyVersion{}, testCase.RestoreCryptoKeyVersionErr) + cfg.KMS.(*mockKMS).On("UpdateCryptoKeyVersion", mock.Anything, mock.Anything).Return(&kmspb.CryptoKeyVersion{}, testCase.UpdateCryptoKeyVersionErr) + + err := cmd.Run(cfg) + if testCase.ExpectedErr != nil { + require.Equal(t, testCase.ExpectedErr, errors.Unwrap(err)) + } else { + require.Error(t, err) + } + }) + } +} diff --git a/pkg/cli/clients_test.go b/pkg/cli/clients_test.go index 687a7a8..3a14ef5 100644 --- a/pkg/cli/clients_test.go +++ b/pkg/cli/clients_test.go @@ -215,7 +215,7 @@ func TestClientUpdateCmd(t *testing.T) { } cfg := &Config{DB: &mockClientDatastore{}} expectedMutations := []*hubauth.ClientMutation{ - &hubauth.ClientMutation{ + { Op: hubauth.ClientMutationOpSetRefreshTokenExpiry, RefreshTokenExpiry: 5 * time.Minute, }, @@ -232,19 +232,19 @@ func TestClientUpdateCmd(t *testing.T) { } cfg := &Config{DB: &mockClientDatastore{}} expectedMutations := []*hubauth.ClientMutation{ - &hubauth.ClientMutation{ + { Op: hubauth.ClientMutationOpSetRefreshTokenExpiry, RefreshTokenExpiry: 5 * time.Minute, }, - &hubauth.ClientMutation{ + { Op: hubauth.ClientMutationOpAddRedirectURI, RedirectURI: "http://localhost:1234", }, - &hubauth.ClientMutation{ + { Op: hubauth.ClientMutationOpAddRedirectURI, RedirectURI: "http://localhost:5678", }, - &hubauth.ClientMutation{ + { Op: hubauth.ClientMutationOpDeleteRedirectURI, RedirectURI: "http://removed-domain:1234", }, diff --git a/pkg/cli/kms/kms.go b/pkg/cli/kms/kms.go index aba10a0..f171568 100644 --- a/pkg/cli/kms/kms.go +++ b/pkg/cli/kms/kms.go @@ -15,7 +15,10 @@ type Client interface { GetPublicKey(context.Context, *kmspb.GetPublicKeyRequest, ...gax.CallOption) (*kmspb.PublicKey, error) // ListCryptoKeyVersions differs from google KMS client interface to get rid of their *kms.CryptoKeyVersionIterator ListCryptoKeyVersions(context.Context, *kmspb.ListCryptoKeyVersionsRequest, ...gax.CallOption) ([]*kmspb.CryptoKeyVersion, error) + CreateCryptoKeyVersion(ctx context.Context, req *kmspb.CreateCryptoKeyVersionRequest, opts ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) DestroyCryptoKeyVersion(context.Context, *kmspb.DestroyCryptoKeyVersionRequest, ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) + RestoreCryptoKeyVersion(ctx context.Context, req *kmspb.RestoreCryptoKeyVersionRequest, opts ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) + UpdateCryptoKeyVersion(ctx context.Context, req *kmspb.UpdateCryptoKeyVersionRequest, opts ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) } type kms struct { @@ -58,3 +61,15 @@ func (k *kms) ListCryptoKeyVersions(ctx context.Context, req *kmspb.ListCryptoKe func (k *kms) DestroyCryptoKeyVersion(ctx context.Context, req *kmspb.DestroyCryptoKeyVersionRequest, opts ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) { return k.c.DestroyCryptoKeyVersion(ctx, req, opts...) } + +func (k *kms) CreateCryptoKeyVersion(ctx context.Context, req *kmspb.CreateCryptoKeyVersionRequest, opts ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) { + return k.c.CreateCryptoKeyVersion(ctx, req, opts...) +} + +func (k *kms) RestoreCryptoKeyVersion(ctx context.Context, req *kmspb.RestoreCryptoKeyVersionRequest, opts ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) { + return k.c.RestoreCryptoKeyVersion(ctx, req, opts...) +} + +func (k *kms) UpdateCryptoKeyVersion(ctx context.Context, req *kmspb.UpdateCryptoKeyVersionRequest, opts ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) { + return k.c.UpdateCryptoKeyVersion(ctx, req, opts...) +} diff --git a/pkg/datastore/audience.go b/pkg/datastore/audience.go index 9690799..30b043c 100644 --- a/pkg/datastore/audience.go +++ b/pkg/datastore/audience.go @@ -165,7 +165,7 @@ func (s *service) MutateAudience(ctx context.Context, url string, mut []*hubauth aud.Policies = aud.Policies[:len(aud.Policies)-1] modified = true } - case hubauth.AudienceMutationSetType: + case hubauth.AudienceMutationOpSetType: if aud.Type == m.Type { continue } diff --git a/pkg/datastore/audience_test.go b/pkg/datastore/audience_test.go index 61d14a2..385f04d 100644 --- a/pkg/datastore/audience_test.go +++ b/pkg/datastore/audience_test.go @@ -347,7 +347,7 @@ func TestAudienceMutate(t *testing.T) { desc: "update type", mut: []*hubauth.AudienceMutation{ { - Op: hubauth.AudienceMutationSetType, + Op: hubauth.AudienceMutationOpSetType, Type: "new-type", }, }, diff --git a/pkg/httpapi/http.go b/pkg/httpapi/http.go index 58da911..74d7d98 100644 --- a/pkg/httpapi/http.go +++ b/pkg/httpapi/http.go @@ -34,6 +34,7 @@ func (clockImpl) Now() time.Time { type Config struct { IdP hubauth.IdPService CookieKey hmacpb.Key + PublicKey []byte ProjectID string Repository string Revision string @@ -87,6 +88,8 @@ func (a *api) ServeHTTP(rw http.ResponseWriter, req *http.Request) { w.Header().Set("Access-Control-Allow-Methods", "GET") w.Header().Set("Access-Control-Max-Age", "86400") w.WriteHeader(http.StatusOK) + case req.Method == "GET" && req.URL.Path == "/public-key": + a.PublicKey(w, req) case req.Method == "GET" && req.URL.Path == "/": http.Redirect(w, req, "https://flynn.io/", http.StatusFound) case req.Method == "GET" && req.URL.Path == "/privacy": @@ -329,11 +332,12 @@ func (a *api) Token(w http.ResponseWriter, req *http.Request) { switch req.Form.Get("grant_type") { case "authorization_code": res, err = a.IdP.ExchangeCode(req.Context(), &hubauth.ExchangeCodeRequest{ - ClientID: req.PostForm.Get("client_id"), - Audience: aud, - RedirectURI: req.PostForm.Get("redirect_uri"), - Code: req.PostForm.Get("code"), - CodeVerifier: req.PostForm.Get("code_verifier"), + ClientID: req.PostForm.Get("client_id"), + Audience: aud, + RedirectURI: req.PostForm.Get("redirect_uri"), + Code: req.PostForm.Get("code"), + CodeVerifier: req.PostForm.Get("code_verifier"), + UserPublicKey: req.PostForm.Get("user_public_key"), }) case "refresh_token": res, err = a.IdP.RefreshToken(req.Context(), &hubauth.RefreshTokenRequest{ @@ -393,6 +397,28 @@ func (a *api) Audiences(w http.ResponseWriter, req *http.Request) { json.NewEncoder(w).Encode(res) } +func (a *api) PublicKey(w http.ResponseWriter, req *http.Request) { + if len(a.Config.PublicKey) == 0 { + a.handleErr(w, req, &hubauth.OAuthError{ + Code: "unsupported_request", + Description: "no public key configured", + }) + return + } + + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Content-Type", "application/json") + + w.WriteHeader(http.StatusOK) + + type key struct { + PublicKey []byte `json:"public-key"` + } + json.NewEncoder(w).Encode(&key{ + PublicKey: a.Config.PublicKey, + }) +} + func (a *api) handleErr(w http.ResponseWriter, req *http.Request, err error) { oe, ok := err.(*hubauth.OAuthError) if !ok { diff --git a/pkg/hubauth/data.go b/pkg/hubauth/data.go index a603b31..73d30dd 100644 --- a/pkg/hubauth/data.go +++ b/pkg/hubauth/data.go @@ -90,15 +90,16 @@ const ( AudienceMutationOpDeleteClientID AudienceMutationOpSetPolicy AudienceMutationOpDeletePolicy - AudienceMutationSetType + AudienceMutationOpSetType ) type AudienceMutation struct { Op AudienceMutationOp - ClientID string - Type string - Policy GoogleUserPolicy + ClientID string + Type string + KeyVersion string + Policy GoogleUserPolicy } type AudiencePolicyMutationOp byte diff --git a/pkg/hubauth/idp.go b/pkg/hubauth/idp.go index 66d4722..7b4d132 100644 --- a/pkg/hubauth/idp.go +++ b/pkg/hubauth/idp.go @@ -38,11 +38,12 @@ type AuthorizeResponse struct { } type ExchangeCodeRequest struct { - ClientID string - RedirectURI string - Audience string - Code string - CodeVerifier string + ClientID string + RedirectURI string + Audience string + Code string + CodeVerifier string + UserPublicKey string } type AccessToken struct { @@ -61,9 +62,10 @@ type AccessToken struct { } type RefreshTokenRequest struct { - ClientID string - Audience string - RefreshToken string + ClientID string + Audience string + RefreshToken string + UserPublicKey string } type ListAudiencesRequest struct { diff --git a/pkg/idp/oauth.go b/pkg/idp/oauth.go index 3815fad..18fdf4a 100644 --- a/pkg/idp/oauth.go +++ b/pkg/idp/oauth.go @@ -2,16 +2,14 @@ package idp import ( "context" - "crypto" "encoding/base64" - "net/url" "strings" "time" "github.com/flynn/hubauth/pkg/clog" "github.com/flynn/hubauth/pkg/hmacpb" "github.com/flynn/hubauth/pkg/hubauth" - "github.com/flynn/hubauth/pkg/kmssign" + "github.com/flynn/hubauth/pkg/idp/token" "github.com/flynn/hubauth/pkg/pb" "github.com/flynn/hubauth/pkg/rp" "github.com/flynn/hubauth/pkg/signpb" @@ -22,22 +20,10 @@ import ( "golang.org/x/sync/errgroup" ) -type AudienceKeyNamer func(audience string) string - const oobRedirectURI = "urn:ietf:wg:oauth:2.0:oob" const codeExpiry = 30 * time.Second const accessTokenDuration = 5 * time.Minute -func AudienceKeyNameFunc(projectID, location, keyRing string) func(string) string { - return func(aud string) string { - u, err := url.Parse(aud) - if err != nil { - return "" - } - return fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s/cryptoKeyVersions/1", projectID, location, keyRing, strings.Replace(u.Host, ".", "_", -1)) - } -} - type clock interface { Now() time.Time } @@ -61,17 +47,15 @@ type idpSteps interface { SignRefreshToken(ctx context.Context, signKey signpb.PrivateKey, t *signedRefreshTokenData) (string, error) RenewRefreshToken(ctx context.Context, clientID, oldTokenID string, oldTokenIssueTime, now time.Time) (*hubauth.RefreshToken, error) VerifyRefreshToken(ctx context.Context, rt *hubauth.RefreshToken, now time.Time) error - SignAccessToken(ctx context.Context, signKey signpb.PrivateKey, t *accessTokenData, now time.Time) (string, error) + BuildAccessToken(ctx context.Context, audience string, t *token.AccessTokenData) (string, string, error) } type idpService struct { - db hubauth.DataStore - rp rp.AuthService - kms kmssign.KMSClient + db hubauth.DataStore + rp rp.AuthService - codeKey hmacpb.Key - refreshKey signpb.Key - audienceKey AudienceKeyNamer + codeKey hmacpb.Key + refreshKey signpb.Key steps idpSteps clock clock @@ -79,16 +63,15 @@ type idpService struct { var _ hubauth.IdPService = (*idpService)(nil) -func New(db hubauth.DataStore, rp rp.AuthService, kms kmssign.KMSClient, codeKey hmacpb.Key, refreshKey signpb.Key, audienceKey AudienceKeyNamer) hubauth.IdPService { +func New(db hubauth.DataStore, rp rp.AuthService, codeKey hmacpb.Key, refreshKey signpb.Key, tokenBuilder token.AccessTokenBuilder) hubauth.IdPService { return &idpService{ - db: db, - rp: rp, - kms: kms, - codeKey: codeKey, - refreshKey: refreshKey, - audienceKey: audienceKey, + db: db, + rp: rp, + codeKey: codeKey, + refreshKey: refreshKey, steps: &steps{ - db: db, + db: db, + builder: tokenBuilder, }, clock: clockImpl{}, } @@ -321,16 +304,29 @@ func (s *idpService) ExchangeCode(parentCtx context.Context, req *hubauth.Exchan }) var accessToken string + var tokenType string g.Go(func() (err error) { if req.Audience == "" { return nil } - signKey := kmssign.NewPrivateKey(s.kms, s.audienceKey(req.Audience), crypto.SHA256) - accessToken, err = s.steps.SignAccessToken(ctx, signKey, &accessTokenData{ - clientID: req.ClientID, - userID: codeInfo.UserId, - userEmail: codeInfo.UserEmail, - }, now) + + var userPublicKey []byte + if len(req.UserPublicKey) > 0 { + var err error + userPublicKey, err = base64Decode(req.UserPublicKey) + if err != nil { + return fmt.Errorf("idp: invalid public key: %v", err) + } + } + + accessToken, tokenType, err = s.steps.BuildAccessToken(ctx, req.Audience, &token.AccessTokenData{ + ClientID: req.ClientID, + UserID: codeInfo.UserId, + UserEmail: codeInfo.UserEmail, + UserPublicKey: userPublicKey, + IssueTime: now, + ExpireTime: now.Add(accessTokenDuration), + }) return err }) @@ -353,7 +349,7 @@ func (s *idpService) ExchangeCode(parentCtx context.Context, req *hubauth.Exchan res.AccessToken = res.RefreshToken res.ExpiresIn = res.RefreshTokenExpiresIn } else { - res.TokenType = "Bearer" + res.TokenType = tokenType res.ExpiresIn = int(accessTokenDuration / time.Second) } return res, nil @@ -395,16 +391,29 @@ func (s *idpService) RefreshToken(ctx context.Context, req *hubauth.RefreshToken }) var accessToken string + var tokenType string g.Go(func() (err error) { if req.Audience == "" { return nil } - signKey := kmssign.NewPrivateKey(s.kms, s.audienceKey(req.Audience), crypto.SHA256) - accessToken, err = s.steps.SignAccessToken(ctx, signKey, &accessTokenData{ - clientID: req.ClientID, - userID: oldToken.UserID, - userEmail: oldToken.UserEmail, - }, now) + + var userPublicKey []byte + if len(req.UserPublicKey) > 0 { + var err error + userPublicKey, err = base64Decode(req.UserPublicKey) + if err != nil { + return fmt.Errorf("idp: invalid public key: %v", err) + } + } + + accessToken, tokenType, err = s.steps.BuildAccessToken(ctx, req.Audience, &token.AccessTokenData{ + ClientID: req.ClientID, + UserID: oldToken.UserID, + UserEmail: oldToken.UserEmail, + UserPublicKey: userPublicKey, + IssueTime: now, + ExpireTime: now.Add(accessTokenDuration), + }) return err }) @@ -426,7 +435,7 @@ func (s *idpService) RefreshToken(ctx context.Context, req *hubauth.RefreshToken res.AccessToken = res.RefreshToken res.ExpiresIn = res.RefreshTokenExpiresIn } else { - res.TokenType = "Bearer" + res.TokenType = tokenType res.ExpiresIn = int(accessTokenDuration / time.Second) } return res, nil diff --git a/pkg/idp/oauth_test.go b/pkg/idp/oauth_test.go index d513374..c4b09c4 100644 --- a/pkg/idp/oauth_test.go +++ b/pkg/idp/oauth_test.go @@ -2,7 +2,6 @@ package idp import ( "context" - "crypto" "crypto/rand" "errors" "fmt" @@ -14,6 +13,7 @@ import ( "github.com/flynn/hubauth/pkg/datastore" "github.com/flynn/hubauth/pkg/hmacpb" "github.com/flynn/hubauth/pkg/hubauth" + "github.com/flynn/hubauth/pkg/idp/token" "github.com/flynn/hubauth/pkg/kmssign" "github.com/flynn/hubauth/pkg/kmssign/kmssim" "github.com/flynn/hubauth/pkg/pb" @@ -40,10 +40,6 @@ func (m *mockAuthService) Exchange(ctx context.Context, rr *rp.RedirectResult) ( return args.Get(0).(*rp.Token), args.Error(1) } -func audienceKeyNamer(s string) string { - return fmt.Sprintf("%s_named", s) -} - type mockSteps struct { mock.Mock } @@ -82,9 +78,9 @@ func (m *mockSteps) SignRefreshToken(ctx context.Context, signKey signpb.Private args := m.Called(ctx, signKey, t) return args.String(0), args.Error(1) } -func (m *mockSteps) SignAccessToken(ctx context.Context, signKey signpb.PrivateKey, t *accessTokenData, now time.Time) (string, error) { - args := m.Called(ctx, signKey, t, now) - return args.String(0), args.Error(1) +func (m *mockSteps) BuildAccessToken(ctx context.Context, audience string, t *token.AccessTokenData) (string, string, error) { + args := m.Called(ctx, audience, t) + return args.String(0), args.String(1), args.Error(2) } func (m *mockSteps) RenewRefreshToken(ctx context.Context, clientID, oldTokenID string, oldTokenIssueTime, now time.Time) (*hubauth.RefreshToken, error) { args := m.Called(ctx, clientID, oldTokenID, oldTokenIssueTime, now) @@ -124,7 +120,7 @@ func newTestIdPService(t *testing.T, kmsKeys ...string) *idpService { refreshKey, err := kmssign.NewKey(context.Background(), kms, refreshKeyName) require.NoError(t, err) - s := New(db, authService, kms, codeKey, refreshKey, audienceKeyNamer).(*idpService) + s := New(db, authService, codeKey, refreshKey, nil).(*idpService) s.steps = &mockSteps{} s.clock = &mockClock{} @@ -669,11 +665,13 @@ func TestExchangeCode(t *testing.T) { }).Return(verifiedCode, nil) idpService.steps.(*mockSteps).On("SaveRefreshToken", mock.Anything, b64CodeID, redirectURI, rtData).Return(client, nil) idpService.steps.(*mockSteps).On("SignRefreshToken", mock.Anything, idpService.refreshKey, signedRTData).Return(refreshToken, nil) - idpService.steps.(*mockSteps).On("SignAccessToken", mock.Anything, kmssign.NewPrivateKey(idpService.kms, audienceKeyNamer(audienceURL), crypto.SHA256), &accessTokenData{ - clientID: clientID, - userID: userID, - userEmail: userEmail, - }, now).Return(accessToken, nil) + idpService.steps.(*mockSteps).On("BuildAccessToken", mock.Anything, audienceURL, &token.AccessTokenData{ + ClientID: clientID, + UserID: userID, + UserEmail: userEmail, + IssueTime: now, + ExpireTime: now.Add(accessTokenDuration), + }).Return(accessToken, testCase.Want.TokenType, nil) req := &hubauth.ExchangeCodeRequest{ ClientID: clientID, @@ -787,7 +785,7 @@ func TestExchangeCodeErrors(t *testing.T) { ExpectedErr: expectedErr, }, { - Desc: "SignAccessToken error", + Desc: "BuildAccessToken error", Code: base64Encode(validCode), SignATErr: expectedErr, ExpectedErr: expectedErr, @@ -804,7 +802,7 @@ func TestExchangeCodeErrors(t *testing.T) { idpService.steps.(*mockSteps).On("VerifyAudience", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(testCase.VerifyAudienceErr) idpService.steps.(*mockSteps).On("SaveRefreshToken", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&hubauth.Client{}, testCase.SaveErr) idpService.steps.(*mockSteps).On("SignRefreshToken", mock.Anything, mock.Anything, mock.Anything).Return("", testCase.SignRTErr) - idpService.steps.(*mockSteps).On("SignAccessToken", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("", testCase.SignATErr) + idpService.steps.(*mockSteps).On("BuildAccessToken", mock.Anything, mock.Anything, mock.Anything).Return("", "", testCase.SignATErr) req := &hubauth.ExchangeCodeRequest{ Code: testCase.Code, @@ -898,12 +896,13 @@ func TestRefreshToken(t *testing.T) { }, ExpiryTime: expireTimeProto.AsTime(), }).Return(newRefreshTokenStr, nil) - signKey := kmssign.NewPrivateKey(idpService.kms, audienceKeyNamer(testCase.AudienceURL), crypto.SHA256) - idpService.steps.(*mockSteps).On("SignAccessToken", mock.Anything, signKey, &accessTokenData{ - clientID: b64ClientID, - userID: userID, - userEmail: userEmail, - }, now).Return(newAccessTokenStr, nil) + idpService.steps.(*mockSteps).On("BuildAccessToken", mock.Anything, testCase.AudienceURL, &token.AccessTokenData{ + ClientID: b64ClientID, + UserID: userID, + UserEmail: userEmail, + IssueTime: now, + ExpireTime: now.Add(accessTokenDuration), + }).Return(newAccessTokenStr, testCase.Want.TokenType, nil) oldTokenSigned, err := signpb.SignMarshal(context.Background(), idpService.refreshKey, &pb.RefreshToken{ Key: oldTokenID, @@ -979,7 +978,7 @@ func TestRefreshTokenStepErrors(t *testing.T) { ExpectedErr: expectedErr, }, { - Desc: "SignAccessToken error", + Desc: "BuildAccessToken error", SignATErr: expectedErr, ExpectedErr: expectedErr, }, @@ -1006,7 +1005,7 @@ func TestRefreshTokenStepErrors(t *testing.T) { idpService.steps.(*mockSteps).On("VerifyAudience", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(testCase.VerifyAudienceErr) idpService.steps.(*mockSteps).On("RenewRefreshToken", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&hubauth.RefreshToken{}, testCase.RenewRTErr) idpService.steps.(*mockSteps).On("SignRefreshToken", mock.Anything, mock.Anything, mock.Anything).Return("", testCase.SignRTErr) - idpService.steps.(*mockSteps).On("SignAccessToken", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("", testCase.SignATErr) + idpService.steps.(*mockSteps).On("BuildAccessToken", mock.Anything, mock.Anything, mock.Anything).Return("", "", testCase.SignATErr) _, err = idpService.RefreshToken(context.Background(), req) require.Equal(t, testCase.ExpectedErr, err) @@ -1015,12 +1014,6 @@ func TestRefreshTokenStepErrors(t *testing.T) { } func prepareInvalidRefreshTokenTestCases(t *testing.T, idpService *idpService, wrongKeyName string) []*invalidRefreshTokenTestCase { - wrongKey, err := kmssign.NewKey(context.Background(), idpService.kms, wrongKeyName) - require.NoError(t, err) - - wrongKeyRefreshToken, err := signpb.SignMarshal(context.Background(), wrongKey, &pb.RefreshToken{}) - require.NoError(t, err) - now := time.Now() expiredTime, _ := ptypes.TimestampProto(now.Add(-1 * time.Second)) expiredRefreshToken, err := signpb.SignMarshal(context.Background(), idpService.refreshKey, &pb.RefreshToken{ @@ -1045,13 +1038,6 @@ func prepareInvalidRefreshTokenTestCases(t *testing.T, idpService *idpService, w Description: "invalid refresh_token", }, }, - { - RefreshToken: base64Encode(wrongKeyRefreshToken), - Err: &hubauth.OAuthError{ - Code: "invalid_grant", - Description: "invalid refresh_token", - }, - }, { RefreshToken: base64Encode(expiredRefreshToken), Err: &hubauth.OAuthError{ diff --git a/pkg/idp/steps.go b/pkg/idp/steps.go index 5a2c6be..8e19815 100644 --- a/pkg/idp/steps.go +++ b/pkg/idp/steps.go @@ -9,6 +9,7 @@ import ( "github.com/flynn/hubauth/pkg/clog" "github.com/flynn/hubauth/pkg/hmacpb" "github.com/flynn/hubauth/pkg/hubauth" + "github.com/flynn/hubauth/pkg/idp/token" "github.com/flynn/hubauth/pkg/pb" "github.com/flynn/hubauth/pkg/signpb" "github.com/golang/protobuf/ptypes" @@ -19,7 +20,8 @@ import ( ) type steps struct { - db hubauth.DataStore + db hubauth.DataStore + builder token.AccessTokenBuilder } var _ idpSteps = (*steps)(nil) @@ -349,37 +351,21 @@ func (s *steps) VerifyRefreshToken(ctx context.Context, rt *hubauth.RefreshToken return nil } -type accessTokenData struct { - clientID string - userID string - userEmail string -} - -func (s *steps) SignAccessToken(ctx context.Context, signKey signpb.PrivateKey, t *accessTokenData, now time.Time) (token string, err error) { - ctx, span := trace.StartSpan(ctx, "idp.SignAccessToken") +func (s *steps) BuildAccessToken(ctx context.Context, audience string, t *token.AccessTokenData) (token string, tokenType string, err error) { + ctx, span := trace.StartSpan(ctx, "idp.BuildAccessToken") span.AddAttributes( - trace.StringAttribute("client_id", t.clientID), - trace.StringAttribute("user_id", t.userID), - trace.StringAttribute("user_email", t.userEmail), + trace.StringAttribute("client_id", t.ClientID), + trace.StringAttribute("user_id", t.UserID), + trace.StringAttribute("user_email", t.UserEmail), ) defer span.End() - exp, _ := ptypes.TimestampProto(now.Add(accessTokenDuration)) - iss, _ := ptypes.TimestampProto(now) - msg := &pb.AccessToken{ - ClientId: t.clientID, - UserId: t.userID, - UserEmail: t.userEmail, - IssueTime: iss, - ExpireTime: exp, - } - tokenBytes, err := signpb.SignMarshal(ctx, signKey, msg) + tokenBytes, err := s.builder.Build(ctx, audience, t) if err != nil { - return "", fmt.Errorf("idp: error signing access token: %w", err) + return "", "", fmt.Errorf("idp: error building access token: %w", err) } - idBytes := sha256.Sum256(tokenBytes) - token = base64.URLEncoding.EncodeToString(tokenBytes) + idBytes := sha256.Sum256(tokenBytes) accessTokenID := base64Encode(idBytes[:]) span.AddAttributes(trace.StringAttribute("access_token_id", accessTokenID)) @@ -387,5 +373,6 @@ func (s *steps) SignAccessToken(ctx context.Context, signKey signpb.PrivateKey, zap.String("issued_access_token_id", accessTokenID), zap.Duration("issued_access_token_expires_in", accessTokenDuration), ) - return token, nil + + return base64.URLEncoding.EncodeToString(tokenBytes), s.builder.TokenType(), nil } diff --git a/pkg/idp/steps_test.go b/pkg/idp/steps_test.go index 46865c6..1c3ba4e 100644 --- a/pkg/idp/steps_test.go +++ b/pkg/idp/steps_test.go @@ -12,21 +12,44 @@ import ( "github.com/flynn/hubauth/pkg/datastore" "github.com/flynn/hubauth/pkg/hmacpb" "github.com/flynn/hubauth/pkg/hubauth" + "github.com/flynn/hubauth/pkg/idp/token" "github.com/flynn/hubauth/pkg/kmssign" "github.com/flynn/hubauth/pkg/kmssign/kmssim" "github.com/flynn/hubauth/pkg/pb" "github.com/flynn/hubauth/pkg/signpb" "github.com/golang/protobuf/ptypes" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "google.golang.org/protobuf/proto" ) +const ( + testAudienceName = "audienceXYZ" +) + +type mockAccessTokenBuilder struct { + mock.Mock +} + +var _ token.AccessTokenBuilder = (*mockAccessTokenBuilder)(nil) + +func (m *mockAccessTokenBuilder) Build(ctx context.Context, audience string, t *token.AccessTokenData) ([]byte, error) { + args := m.Called(ctx, audience, t) + return args.Get(0).([]byte), args.Error(1) +} + +func (m *mockAccessTokenBuilder) TokenType() string { + args := m.Called() + return args.String(0) +} + func newTestSteps(t *testing.T) *steps { dsc, err := gdatastore.NewClient(context.Background(), "test") require.NoError(t, err) return &steps{ - db: datastore.New(dsc), + db: datastore.New(dsc), + builder: &mockAccessTokenBuilder{}, } } @@ -751,39 +774,31 @@ func TestVerifyRefreshTokenErrors(t *testing.T) { } } -func TestSignAccessToken(t *testing.T) { +func TestBuildAccessToken(t *testing.T) { s := newTestSteps(t) - signKeyName := "refreshKey" - kms := kmssim.NewClient([]string{signKeyName}) - signKey, err := kmssign.NewKey(context.Background(), kms, signKeyName) - require.NoError(t, err) - now := time.Now() - data := &accessTokenData{ - clientID: "clientID", - userID: "userID", - userEmail: "userEmail", + data := &token.AccessTokenData{ + ClientID: "clientID", + UserID: "userID", + UserEmail: "userEmail", + IssueTime: now, + ExpireTime: now.Add(accessTokenDuration), } - accessToken, err := s.SignAccessToken(context.Background(), signKey, data, now) + expectedAccessToken := []byte("expected-access-token") + expectedTokenType := "MockBearer" + + s.builder.(*mockAccessTokenBuilder).On("Build", mock.Anything, testAudienceName, data).Return(expectedAccessToken, nil) + s.builder.(*mockAccessTokenBuilder).On("TokenType").Return(expectedTokenType) + + accessToken, tokenType, err := s.BuildAccessToken(context.Background(), testAudienceName, data) require.NoError(t, err) require.NotEmpty(t, accessToken) - - got := new(pb.AccessToken) + require.Equal(t, expectedTokenType, tokenType) accessTokenBytes, err := base64Decode(accessToken) require.NoError(t, err) - - require.NoError(t, signpb.VerifyUnmarshal(signKey, accessTokenBytes, got)) - require.Equal(t, data.clientID, got.ClientId) - require.Equal(t, data.userID, got.UserId) - require.Equal(t, data.userEmail, got.UserEmail) - - nowPb, _ := ptypes.TimestampProto(now) - require.Equal(t, nowPb, got.IssueTime) - - expirePb, _ := ptypes.TimestampProto(now.Add(accessTokenDuration)) - require.Equal(t, expirePb, got.ExpireTime) + require.Equal(t, expectedAccessToken, accessTokenBytes) } diff --git a/pkg/idp/token/bearer.go b/pkg/idp/token/bearer.go new file mode 100644 index 0000000..e56ee50 --- /dev/null +++ b/pkg/idp/token/bearer.go @@ -0,0 +1,54 @@ +package token + +import ( + "context" + "crypto" + "fmt" + + "github.com/flynn/hubauth/pkg/kmssign" + "github.com/flynn/hubauth/pkg/pb" + "github.com/flynn/hubauth/pkg/signpb" + "github.com/golang/protobuf/ptypes" +) + +type bearerBuilder struct { + kms kmssign.KMSClient + audienceKey kmssign.AudienceKeyNamer +} + +var _ AccessTokenBuilder = (*bearerBuilder)(nil) + +func NewBearerBuilder(kms kmssign.KMSClient, audienceKey kmssign.AudienceKeyNamer) AccessTokenBuilder { + return &bearerBuilder{ + kms: kms, + audienceKey: audienceKey, + } +} + +func (b *bearerBuilder) Build(ctx context.Context, audience string, t *AccessTokenData) ([]byte, error) { + keyName, err := b.audienceKey(audience) + if err != nil { + return nil, fmt.Errorf("token: failed to get audience key name: %w", err) + } + signKey := kmssign.NewPrivateKey(b.kms, keyName, crypto.SHA256) + + exp, _ := ptypes.TimestampProto(t.ExpireTime) + iss, _ := ptypes.TimestampProto(t.IssueTime) + msg := &pb.AccessToken{ + ClientId: t.ClientID, + UserId: t.UserID, + UserEmail: t.UserEmail, + IssueTime: iss, + ExpireTime: exp, + } + tokenBytes, err := signpb.SignMarshal(ctx, signKey, msg) + if err != nil { + return nil, fmt.Errorf("token: error signing access token: %w", err) + } + + return tokenBytes, nil +} + +func (b *bearerBuilder) TokenType() string { + return "Bearer" +} diff --git a/pkg/idp/token/bearer_test.go b/pkg/idp/token/bearer_test.go new file mode 100644 index 0000000..5f68483 --- /dev/null +++ b/pkg/idp/token/bearer_test.go @@ -0,0 +1,59 @@ +package token + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/flynn/hubauth/pkg/kmssign" + "github.com/flynn/hubauth/pkg/kmssign/kmssim" + "github.com/flynn/hubauth/pkg/pb" + "github.com/flynn/hubauth/pkg/signpb" + "github.com/golang/protobuf/ptypes" + "github.com/stretchr/testify/require" +) + +func audienceKeyNamer(s string) (string, error) { + return fmt.Sprintf("%s_named", s), nil +} + +func TestSignedPBBuilder(t *testing.T) { + audienceName := "audience_url" + audienceKeyName, _ := audienceKeyNamer(audienceName) + kms := kmssim.NewClient([]string{audienceKeyName}) + + builder := NewBearerBuilder(kms, audienceKeyNamer) + + signKey, err := kmssign.NewKey(context.Background(), kms, audienceKeyName) + require.NoError(t, err) + + now := time.Now() + ctx := context.Background() + + accessTokenDuration := 5 * time.Minute + + data := &AccessTokenData{ + ClientID: "clientID", + UserEmail: "userEmail", + UserID: "userID", + IssueTime: now, + ExpireTime: now.Add(accessTokenDuration), + } + + accessTokenBytes, err := builder.Build(ctx, audienceName, data) + require.NoError(t, err) + + got := new(pb.AccessToken) + require.NoError(t, signpb.VerifyUnmarshal(signKey, accessTokenBytes, got)) + + require.Equal(t, data.ClientID, got.ClientId) + require.Equal(t, data.UserID, got.UserId) + require.Equal(t, data.UserEmail, got.UserEmail) + + nowPb, _ := ptypes.TimestampProto(now) + require.Equal(t, nowPb, got.IssueTime) + + expirePb, _ := ptypes.TimestampProto(now.Add(accessTokenDuration)) + require.Equal(t, expirePb, got.ExpireTime) +} diff --git a/pkg/idp/token/biscuit.go b/pkg/idp/token/biscuit.go new file mode 100644 index 0000000..819a5f4 --- /dev/null +++ b/pkg/idp/token/biscuit.go @@ -0,0 +1,69 @@ +package token + +import ( + "context" + "crypto" + "encoding/base64" + "errors" + "fmt" + + "github.com/flynn/biscuit-go/cookbook/signedbiscuit" + "github.com/flynn/biscuit-go/sig" + "github.com/flynn/hubauth/pkg/kmssign" +) + +var ( + ErrPublicKeyRequired = errors.New("token: a public key is required") +) + +type biscuitBuilder struct { + kms kmssign.KMSClient + audienceKey kmssign.AudienceKeyNamer + rootKeyPair sig.Keypair +} + +func NewBiscuitBuilder(kms kmssign.KMSClient, audienceKey kmssign.AudienceKeyNamer, rootKeyPair sig.Keypair) AccessTokenBuilder { + return &biscuitBuilder{ + kms: kms, + audienceKey: audienceKey, + rootKeyPair: rootKeyPair, + } +} + +func (b *biscuitBuilder) Build(ctx context.Context, audience string, t *AccessTokenData) ([]byte, error) { + if len(t.UserPublicKey) == 0 { + return nil, ErrPublicKeyRequired + } + keyName, err := b.audienceKey(audience) + if err != nil { + return nil, fmt.Errorf("token: failed to get audience key name: %w", err) + } + audienceKey := kmssign.NewPrivateKey(b.kms, keyName, crypto.SHA256) + + meta := &signedbiscuit.Metadata{ + ClientID: t.ClientID, + UserID: t.UserID, + UserEmail: t.UserEmail, + IssueTime: t.IssueTime, + } + + return signedbiscuit.GenerateSignable(b.rootKeyPair, audience, audienceKey, t.UserPublicKey, t.ExpireTime, meta) +} + +func (b *biscuitBuilder) TokenType() string { + return "Biscuit" +} + +func DecodeB64PrivateKey(b64key string) (sig.Keypair, error) { + var kp sig.Keypair + privKey, err := base64.StdEncoding.DecodeString(b64key) + if err != nil { + return kp, fmt.Errorf("failed to decode b64 key: %w", err) + } + rootPrivateKey, err := sig.NewPrivateKey(privKey) + if err != nil { + return kp, fmt.Errorf("failed to create biscuit private key: %w", err) + } + kp = sig.NewKeypair(rootPrivateKey) + return kp, nil +} diff --git a/pkg/idp/token/biscuit_test.go b/pkg/idp/token/biscuit_test.go new file mode 100644 index 0000000..e2c0ec4 --- /dev/null +++ b/pkg/idp/token/biscuit_test.go @@ -0,0 +1,63 @@ +package token + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "encoding/base64" + "testing" + "time" + + "github.com/flynn/biscuit-go" + "github.com/flynn/biscuit-go/sig" + "github.com/flynn/hubauth/pkg/kmssign/kmssim" + "github.com/stretchr/testify/require" +) + +func TestBiscuitBuilder(t *testing.T) { + audience := "https://audience.url" + audienceKeyName, _ := audienceKeyNamer(audience) + kmsClient := kmssim.NewClient([]string{audienceKeyName}) + rootKeyPair := sig.GenerateKeypair(rand.Reader) + + builder := NewBiscuitBuilder(kmsClient, audienceKeyNamer, rootKeyPair) + + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + userPublicKey, err := x509.MarshalPKIXPublicKey(&priv.PublicKey) + require.NoError(t, err) + + now := time.Now() + accessTokenData := &AccessTokenData{ + ClientID: "clientID", + ExpireTime: now.Add(1 * time.Minute), + IssueTime: now, + UserEmail: "user@email", + UserID: "userID", + } + _, err = builder.Build(context.Background(), audience, accessTokenData) + require.Equal(t, ErrPublicKeyRequired, err) + + accessTokenData.UserPublicKey = userPublicKey + token, err := builder.Build(context.Background(), audience, accessTokenData) + require.NoError(t, err) + require.NotEmpty(t, token) + + b, err := biscuit.Unmarshal(token) + require.NoError(t, err) + + _, err = b.Verify(rootKeyPair.Public()) + require.NoError(t, err) +} + +func TestDecodeB64PrivateKey(t *testing.T) { + expectedKP := sig.GenerateKeypair(rand.Reader) + b64key := base64.StdEncoding.EncodeToString(expectedKP.Private().Bytes()) + + kp, err := DecodeB64PrivateKey(b64key) + require.NoError(t, err) + require.Equal(t, expectedKP.Private().Bytes(), kp.Private().Bytes()) + require.Equal(t, expectedKP.Public().Bytes(), kp.Public().Bytes()) +} diff --git a/pkg/idp/token/builder.go b/pkg/idp/token/builder.go new file mode 100644 index 0000000..0da8c9a --- /dev/null +++ b/pkg/idp/token/builder.go @@ -0,0 +1,20 @@ +package token + +import ( + "context" + "time" +) + +type AccessTokenData struct { + ClientID string + UserID string + UserEmail string + UserPublicKey []byte + IssueTime time.Time + ExpireTime time.Time +} + +type AccessTokenBuilder interface { + Build(ctx context.Context, audience string, t *AccessTokenData) ([]byte, error) + TokenType() string +} diff --git a/pkg/kmssign/kms.go b/pkg/kmssign/kms.go index 8671e3e..e643b58 100644 --- a/pkg/kmssign/kms.go +++ b/pkg/kmssign/kms.go @@ -8,6 +8,8 @@ import ( "encoding/pem" "io" "math/big" + "net/url" + "strings" gax "github.com/googleapis/gax-go/v2" "golang.org/x/crypto/cryptobyte" @@ -16,6 +18,31 @@ import ( kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1" ) +type AudienceKeyNamer func(audience string) (string, error) + +// ForcedAudiencesKeyVersion lists audienceURLs with a key version resource name +// when provided to an AudienceKeyNameFunc, it allows overriding a default audience +// key version with the one from the map. +type ForcedAudiencesKeyVersion map[string]string + +// AudienceKeyNameFunc returns the GCP KMS resource name of the audience key. +// The default version returned is 1, and it can be overridden by adding the audience URL +// and the new key to the ForcedAudiencesKeyVersion map. +func AudienceKeyNameFunc(forcedAudiencesKeyVersion ForcedAudiencesKeyVersion, projectID, location, keyRing string) func(string) (string, error) { + return func(aud string) (string, error) { + u, err := url.Parse(aud) + if err != nil { + return "", err + } + + if keyName, ok := forcedAudiencesKeyVersion[aud]; ok { + return keyName, nil + } + + return fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s/cryptoKeyVersions/1", projectID, location, keyRing, strings.Replace(u.Host, ".", "_", -1)), nil + } +} + type KMSClient interface { AsymmetricSign(ctx context.Context, req *kmspb.AsymmetricSignRequest, opts ...gax.CallOption) (*kmspb.AsymmetricSignResponse, error) GetPublicKey(ctx context.Context, req *kmspb.GetPublicKeyRequest, opts ...gax.CallOption) (*kmspb.PublicKey, error) diff --git a/pkg/kmssign/kms_test.go b/pkg/kmssign/kms_test.go index fd910f1..fb4273e 100644 --- a/pkg/kmssign/kms_test.go +++ b/pkg/kmssign/kms_test.go @@ -304,3 +304,67 @@ func TestVerifyPanicWithEmptyPubkey(t *testing.T) { t.Errorf("did not panic") } + +func TestAudienceKeyNameFunc(t *testing.T) { + testCases := []struct { + Desc string + ForcedVersions ForcedAudiencesKeyVersion + ProjectID string + Location string + KeyRing string + Audience string + ExpectedKeyName string + ExpectErr bool + }{ + { + Desc: "default key is version 1", + ForcedVersions: map[string]string{}, + ProjectID: "projectID", + Location: "location", + KeyRing: "keyRing", + Audience: "https://audience.url", + ExpectedKeyName: "projects/projectID/locations/location/keyRings/keyRing/cryptoKeys/audience_url/cryptoKeyVersions/1", + ExpectErr: false, + }, + { + Desc: "invalid audience url", + ForcedVersions: map[string]string{}, + Audience: "://audience.url", + ExpectedKeyName: "", + ExpectErr: true, + }, + { + Desc: "overriden version", + ForcedVersions: map[string]string{"https://audience.url": "new key version"}, + ProjectID: "projectID", + Location: "location", + KeyRing: "keyRing", + Audience: "https://audience.url", + ExpectedKeyName: "new key version", + ExpectErr: false, + }, + { + Desc: "overriden version different audience", + ForcedVersions: map[string]string{"https://another.url": "new key version"}, + ProjectID: "projectID", + Location: "location", + KeyRing: "keyRing", + Audience: "https://audience.url", + ExpectedKeyName: "projects/projectID/locations/location/keyRings/keyRing/cryptoKeys/audience_url/cryptoKeyVersions/1", + ExpectErr: false, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Desc, func(t *testing.T) { + f := AudienceKeyNameFunc(testCase.ForcedVersions, testCase.ProjectID, testCase.Location, testCase.KeyRing) + keyName, err := f(testCase.Audience) + if testCase.ExpectErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + require.Equal(t, testCase.ExpectedKeyName, keyName) + }) + } +} diff --git a/script/setup-hubauth.sh b/script/setup-hubauth.sh new file mode 100755 index 0000000..831f697 --- /dev/null +++ b/script/setup-hubauth.sh @@ -0,0 +1,193 @@ +#!/usr/bin/env bash + +set -ueo pipefail + +KMS_LOCATION=${KMS_LOCATION-"global"} +KMS_KEYRING=${KMS_KEYRING-"hubauth"} +TOKEN_TYPE=${TOKEN_TYPE-"Bearer"} # || Biscuit +PROJECT_ID=${PROJECT_ID-""} + +EXPECTED_HUBAUTH_EXT_ENV=( + # env provided or script default + "KMS_LOCATION" + "KMS_KEYRING" + "TOKEN_TYPE" + # env provided or auto populated using gcloud + "PROJECT_ID" + # auto populated using gcloud + "BASE_URL" + # auto generated kms key if not existing + "REFRESH_KEY" + # auto generated secrets if not existing + "COOKIE_KEY_SECRET" + "CODE_KEY_SECRET" + "BISCUIT_ROOT_PRIVKEY" # only when TOKEN_TYPE == "Biscuit" + # need user input + "RP_GOOGLE_CLIENT_ID" + "RP_GOOGLE_CLIENT_SECRET" +) + +EXPECTED_HUBAUTH_INT_ENV=( + # env provided or auto populated using gcloud + "PROJECT_ID" +) + + +case "${TOKEN_TYPE}" in +"Bearer" | "Biscuit") + ;; +*) + echo "invalid TOKEN_TYPE \"${TOKEN_TYPE}\", must be either \"Bearer\" or \"Biscuit\"" + exit 1 + ;; +esac + +if [ $# -lt 2 ] || [[ " $@ " =~ "-h" ]] || [[ " $@ " =~ "--help" ]]; then + echo "Wizard to check and configure hubauth services and their GCP dependencies" + echo -e "\nUsage:\n$0 [-h|--help]\n" + echo -e "\nARGUMENTS:" + echo -e "\tAPP: the application to configure (\"hubauth-int\" or \"hubauth-ext\")" + echo -e "\tREGION: a GCP region where the application exists (ie: \"us-central1\")" + echo -e "\t-h | --help: print this help" + echo -e "\nENV:" + echo -e "\tPROJECT_ID (defaults to current gcloud active config project)" + echo -e "\t Prompts for confirmation when not specified" + echo -e "\tKMS_LOCATION (defaults to \"${KMS_LOCATION}\")" + echo -e "\tKMS_KEYRING (defaults to \"${KMS_KEYRING}\")" + echo -e "\tTOKEN_TYPE (defaults to \"${TOKEN_TYPE}\", accepts \"Bearer\" or \"Biscuit\")\n" + exit 1 +fi + +APP=$1 +REGION=$2 + +if [ -z "${PROJECT_ID}" ]; then + PROJECT_ID=$(gcloud config get-value project) + read -p "Current project: ${PROJECT_ID}, confirm ? [Yn]: " + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "operation cancelled" + exit 0 + fi +fi + +GCLOUD="gcloud --project ${PROJECT_ID}" + +case "${APP}" in +"hubauth-ext") + EXPECTED_APP_ENV=${EXPECTED_HUBAUTH_EXT_ENV[@]} + ;; +"hubauth-int") + EXPECTED_APP_ENV=${EXPECTED_HUBAUTH_INT_ENV[@]} + ;; +*) + echo "invalid app, it must be either hubauth-ext or hubauth-int" + exit 1 + ;; +esac + + +SPECS=$(${GCLOUD} run services describe "${APP}-${REGION}" --platform managed --region "${REGION}" --format json) + +CONTAINER_SPECS=$(echo "${SPECS}" | jq '.spec.template.spec.containers[0]') +IMAGE=$(echo "${CONTAINER_SPECS}" | jq -r '.image') + +NEW_ENVS=() +for env in ${EXPECTED_APP_ENV[@]}; do + EXISTS=$(echo "${CONTAINER_SPECS}" | jq '.env[] | . | select(.name | contains('\"${env}\"'))') + if [ ! -z "${EXISTS}" ]; then + continue + fi + + case "${env}" in + "KMS_LOCATION") + value="${KMS_LOCATION}" + ;; + "KMS_KEYRING") + value="${KMS_KEYRING}" + ;; + "TOKEN_TYPE") + value="${TOKEN_TYPE}" + ;; + "PROJECT_ID") + value=${PROJECT_ID} + ;; + "BASE_URL") + value=$(echo "${SPECS}" | jq -r '.status.url // empty') + ;; + "COOKIE_KEY_SECRET" | "CODE_KEY_SECRET") + value=$(${GCLOUD} secrets describe "${env}" --format 'value("name")' || true) + if [ -z "${value}" ]; then + head -c 32 /dev/random | base64 -w0 | ${GCLOUD} secrets create ${env} --data-file - + value=$(${GCLOUD} secrets describe "${env}" --format 'value("name")') + fi + value+="/versions/latest" + ;; + "BISCUIT_ROOT_PRIVKEY") + if [ "${TOKEN_TYPE}" != "Biscuit" ]; then + continue + fi + value=$(${GCLOUD} secrets describe "${env}" --format 'value("name")' || true) + if [ -z "${value}" ]; then + head -c 32 /dev/random | base64 -w0 | ${GCLOUD} secrets create ${env} --data-file - + value=$(${GCLOUD} secrets describe "${env}" --format 'value("name")') + fi + value+="/versions/latest" + ;; + "REFRESH_KEY") + value=$(${GCLOUD} kms keys versions list --key "${env}" --keyring "${KMS_KEYRING}" --location "${KMS_LOCATION}" --format 'value("name")' 2>/dev/null | sort -r | head -n1 || true) + if [ -z "${value}" ]; then + ${GCLOUD} kms keys create "${env}" \ + --keyring "${KMS_KEYRING}" \ + --location "${KMS_LOCATION}" \ + --purpose "asymmetric-signing" \ + --default-algorithm "ec-sign-p256-sha256" \ + --protection-level "software" + value=$(${GCLOUD} kms keys versions list --key "${env}" --keyring "${KMS_KEYRING}" --location "${KMS_LOCATION}" --format 'value("name")') + fi + ;; + *) # default ask for user input + echo -n "enter value for ${env}: " + read value + ;; + esac + + if [ ! -z "${value}" ]; then + NEW_ENVS+=("${env}=${value}") + fi +done + +if [ ${#NEW_ENVS[@]} -gt 0 ]; then + ENV_STR=$(IFS=,; printf '%s' "${NEW_ENVS[*]}") + ${GCLOUD} run deploy "${APP}-${REGION}" --platform managed --region "${REGION}" --image "${IMAGE}" --update-env-vars "${ENV_STR}" +fi + +BASE_URL=$(${GCLOUD} run services describe "${APP}-${REGION}" --platform managed --region "${REGION}" --format json | jq -r '.status.url') + +# we need a first successful deployment in order to obtain the service URL. +# so when it was empty, and the above deploy succeeded, we can immediately redeploy setting the BASE_URL env +if [ -z "$(echo ${SPECS} | jq -r '.status.url // empty')" ] && [[ "${EXPECTED_APP_ENV[@]}" =~ "BASE_URL" ]]; then + echo "just obtained a service url for the first time, setting BASE_URL env variable..." + ${GCLOUD} run deploy "${APP}-${REGION}" --platform managed --region "${REGION}" --image "${IMAGE}" --update-env-vars "BASE_URL=${BASE_URL}" +fi + +# Create a scheduler invoking hubauth-int /cron endpoint +# Using below service account for authentication (or create it if needed) +SA_NAME="hubauth-scheduler" +SA_EMAIL="${SA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com" + +if [ ${APP} = "hubauth-int" ] && [ ! -z ${BASE_URL} ]; then + if [ -z "$(${GCLOUD} scheduler jobs describe "${APP}-${REGION}-CRON" 2>/dev/null || true)" ]; then + if [ -z "$(${GCLOUD} iam service-accounts describe "${SA_EMAIL}" 2>/dev/null || true)" ]; then + ${GCLOUD} iam service-accounts create "${SA_NAME}" --display-name="GCloud Scheduler SA" + ${GCLOUD} projects add-iam-policy-binding --quiet "${PROJECT_ID}" --member "serviceAccount:${SA_EMAIL}" --role "roles/run.invoker" + fi + + ${GCLOUD} scheduler jobs create http "${APP}-${REGION}-CRON" \ + --description "sync & cleanup task for hubauth" \ + --schedule "0 */1 * * *" \ + --uri "${BASE_URL}/cron" \ + --http-method "get" \ + --oidc-service-account-email "${SA_EMAIL}" \ + --oidc-token-audience "${BASE_URL}" + fi +fi