diff --git a/go.mod b/go.mod index 6141219e..7cf43289 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,9 @@ module github.com/conductorone/baton-postgresql -go 1.20 +go 1.22 require ( - github.com/conductorone/baton-sdk v0.1.22 + github.com/conductorone/baton-sdk v0.1.33 github.com/georgysavva/scany v1.2.1 github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 github.com/jackc/pgx/v4 v4.18.1 diff --git a/go.sum b/go.sum index cc69a368..94859448 100644 --- a/go.sum +++ b/go.sum @@ -98,8 +98,8 @@ github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/cockroach-go/v2 v2.2.0 h1:/5znzg5n373N/3ESjHF5SMLxiW4RKB05Ql//KWfeTFs= github.com/cockroachdb/cockroach-go/v2 v2.2.0/go.mod h1:u3MiKYGupPPjkn3ozknpMUpxPaNLTFWAya419/zv6eI= -github.com/conductorone/baton-sdk v0.1.22 h1:9IX0q8oPVfuHyvXdLbxCRGgqhjp/SqGv8T1RbiM1Tps= -github.com/conductorone/baton-sdk v0.1.22/go.mod h1:1VMycIep+HU8JXef2wenT3ECzx1w3Jr3KDQG+L6Mv30= +github.com/conductorone/baton-sdk v0.1.33 h1:0kWu7yCEux+KgUOs1xoo0TFARwN8Tc9/KOP6c4HqdFs= +github.com/conductorone/baton-sdk v0.1.33/go.mod h1:1VMycIep+HU8JXef2wenT3ECzx1w3Jr3KDQG+L6Mv30= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -107,6 +107,7 @@ github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7Do github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/doug-martin/goqu/v9 v9.18.0 h1:/6bcuEtAe6nsSMVK/M+fOiXUNfyFF3yYtE07DBPFMYY= github.com/doug-martin/goqu/v9 v9.18.0/go.mod h1:nf0Wc2/hV3gYK9LiyqIrzBEVGlI8qW3GuDCEobC4wBQ= @@ -121,6 +122,7 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA= github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/georgysavva/scany v1.2.1 h1:91PAMBpwBtDjvn46TaLQmuVhxpAG6p6sjQaU4zPHPSM= @@ -207,6 +209,7 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= @@ -307,10 +310,12 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxv github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= @@ -347,6 +352,7 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b h1:0LFwY6Q3gMACTjAbMZBjXAqTOzOwFaj2Ld6cjeQ7Rig= github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= @@ -357,6 +363,7 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= @@ -435,8 +442,10 @@ go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= +go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= @@ -631,6 +640,7 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= 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= @@ -811,6 +821,7 @@ google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/connectorbuilder/connectorbuilder.go b/vendor/github.com/conductorone/baton-sdk/pkg/connectorbuilder/connectorbuilder.go index 22b7bdf3..ea632cb8 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/connectorbuilder/connectorbuilder.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/connectorbuilder/connectorbuilder.go @@ -188,6 +188,9 @@ func (b *builderImpl) ListResources(ctx context.Context, request *v2.ResourcesSe if err != nil { return nil, fmt.Errorf("error: listing resources failed: %w", err) } + if request.PageToken != "" && request.PageToken == nextPageToken { + return nil, fmt.Errorf("error: listing resources failed: next page token is the same as the current page token. this is most likely a connector bug") + } return &v2.ResourcesServiceListResourcesResponse{ List: out, @@ -210,6 +213,9 @@ func (b *builderImpl) ListEntitlements(ctx context.Context, request *v2.Entitlem if err != nil { return nil, fmt.Errorf("error: listing entitlements failed: %w", err) } + if request.PageToken != "" && request.PageToken == nextPageToken { + return nil, fmt.Errorf("error: listing entitlements failed: next page token is the same as the current page token. this is most likely a connector bug") + } return &v2.EntitlementsServiceListEntitlementsResponse{ List: out, @@ -232,6 +238,9 @@ func (b *builderImpl) ListGrants(ctx context.Context, request *v2.GrantsServiceL if err != nil { return nil, fmt.Errorf("error: listing grants failed: %w", err) } + if request.PageToken != "" && request.PageToken == nextPageToken { + return nil, fmt.Errorf("error: listing grants failed: next page token is the same as the current page token. this is most likely a connector bug") + } return &v2.GrantsServiceListGrantsResponse{ List: out, diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/decoder.go b/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/decoder.go index 8735dc32..ee27fd03 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/decoder.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/decoder.go @@ -179,7 +179,9 @@ func (d *decoder) Read(p []byte) (int, error) { } func (d *decoder) Close() error { - d.zd.Close() + if d.zd != nil { + d.zd.Close() + } return nil } diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/sql_helpers.go b/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/sql_helpers.go index badcef02..63f8356e 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/sql_helpers.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/sql_helpers.go @@ -16,7 +16,7 @@ import ( v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" ) -const maxPageSize = 250 +const maxPageSize = 10000 var allTableDescriptors = []tableDescriptor{ resourceTypes, diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/helpers/helpers.go b/vendor/github.com/conductorone/baton-sdk/pkg/helpers/helpers.go new file mode 100644 index 00000000..b08f6899 --- /dev/null +++ b/vendor/github.com/conductorone/baton-sdk/pkg/helpers/helpers.go @@ -0,0 +1,111 @@ +package helpers + +import ( + "net/http" + "strconv" + "strings" + "time" + + v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func SplitFullName(name string) (string, string) { + names := strings.SplitN(name, " ", 2) + var firstName, lastName string + + switch len(names) { + case 1: + firstName = names[0] + case 2: + firstName = names[0] + lastName = names[1] + } + + return firstName, lastName +} + +func ExtractRateLimitData(statusCode int, header *http.Header) (*v2.RateLimitDescription, error) { + if header == nil { + return nil, nil + } + + var rlstatus v2.RateLimitDescription_Status + + var limit int64 + var err error + limitStr := header.Get("X-Ratelimit-Limit") + if limitStr != "" { + limit, err = strconv.ParseInt(limitStr, 10, 64) + if err != nil { + return nil, err + } + } + + var remaining int64 + remainingStr := header.Get("X-Ratelimit-Remaining") + if remainingStr != "" { + remaining, err = strconv.ParseInt(remainingStr, 10, 64) + if err != nil { + return nil, err + } + if remaining > 0 { + rlstatus = v2.RateLimitDescription_STATUS_OK + } + } + + var resetAt time.Time + reset := header.Get("X-Ratelimit-Reset") + if reset != "" { + res, err := strconv.ParseInt(reset, 10, 64) + if err != nil { + return nil, err + } + + resetAt = time.Now().Add(time.Second * time.Duration(res)) + } + + // If we didn't get any rate limit headers and status code is 429, return some sane defaults + if limit == 0 && remaining == 0 && resetAt.IsZero() && statusCode == http.StatusTooManyRequests { + limit = 1 + remaining = 0 + resetAt = time.Now().Add(time.Second * 60) + rlstatus = v2.RateLimitDescription_STATUS_OVERLIMIT + } + + return &v2.RateLimitDescription{ + Status: rlstatus, + Limit: limit, + Remaining: remaining, + ResetAt: timestamppb.New(resetAt), + }, nil +} + +func IsJSONContentType(contentType string) bool { + if !strings.HasPrefix(contentType, "application") { + return false + } + + if !strings.Contains(contentType, "json") { + return false + } + + return true +} + +var xmlContentTypes []string = []string{ + "text/xml", + "application/xml", +} + +func IsXMLContentType(contentType string) bool { + // there are some janky APIs out there + normalizedContentType := strings.TrimSpace(strings.ToLower(contentType)) + + for _, xmlContentType := range xmlContentTypes { + if normalizedContentType == xmlContentType { + return true + } + } + return false +} diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/sdk/version.go b/vendor/github.com/conductorone/baton-sdk/pkg/sdk/version.go index 46bf4724..f98920c5 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/sdk/version.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/sdk/version.go @@ -1,4 +1,4 @@ package sdk // Version is the current version of the baton SDK. -const Version = "0.0.26" +const Version = "0.1.30" diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/sync/syncer.go b/vendor/github.com/conductorone/baton-sdk/pkg/sync/syncer.go index 32306333..92fef385 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/sync/syncer.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/sync/syncer.go @@ -11,6 +11,8 @@ import ( "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" "go.uber.org/zap" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" "google.golang.org/protobuf/proto" c1zpb "github.com/conductorone/baton-sdk/pb/c1/c1z/v1" @@ -72,6 +74,25 @@ func (s *syncer) handleProgress(ctx context.Context, a *Action, c int) { } } +func shouldWaitAndRetry(ctx context.Context, err error) bool { + if status.Code(err) != codes.Unavailable { + return false + } + + l := ctxzap.Extract(ctx) + l.Error("retrying operation", zap.Error(err)) + + for { + select { + // TODO: this should back off based on error counts + case <-time.After(1 * time.Second): + return true + case <-ctx.Done(): + return false + } + } +} + // Sync starts the syncing process. The sync process is driven by the action stack that is part of the state object. // For each page of data that is required to be fetched from the connector, a new action is pushed on to the stack. Once // an action is completed, it is popped off of the queue. Before procesing each action, we checkpoint the state object @@ -162,28 +183,28 @@ func (s *syncer) Sync(ctx context.Context) error { case SyncResourceTypesOp: err = s.SyncResourceTypes(ctx) - if err != nil { + if err != nil && !shouldWaitAndRetry(ctx, err) { return err } continue case SyncResourcesOp: err = s.SyncResources(ctx) - if err != nil { + if err != nil && !shouldWaitAndRetry(ctx, err) { return err } continue case SyncEntitlementsOp: err = s.SyncEntitlements(ctx) - if err != nil { + if err != nil && !shouldWaitAndRetry(ctx, err) { return err } continue case SyncGrantsOp: err = s.SyncGrants(ctx) - if err != nil { + if err != nil && !shouldWaitAndRetry(ctx, err) { return err } continue @@ -203,7 +224,7 @@ func (s *syncer) Sync(ctx context.Context) error { } err = s.SyncGrantExpansion(ctx) - if err != nil { + if err != nil && !shouldWaitAndRetry(ctx, err) { return err } continue diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/types/resource/user_trait.go b/vendor/github.com/conductorone/baton-sdk/pkg/types/resource/user_trait.go index 0fae16e3..7c104801 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/types/resource/user_trait.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/types/resource/user_trait.go @@ -4,10 +4,11 @@ import ( "fmt" "time" - v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" - "github.com/conductorone/baton-sdk/pkg/annotations" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" + + v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" + "github.com/conductorone/baton-sdk/pkg/annotations" ) type UserTraitOption func(ut *v2.UserTrait) error @@ -20,6 +21,14 @@ func WithStatus(status v2.UserTrait_Status_Status) UserTraitOption { } } +func WithDetailedStatus(status v2.UserTrait_Status_Status, details string) UserTraitOption { + return func(ut *v2.UserTrait) error { + ut.Status = &v2.UserTrait_Status{Status: status, Details: details} + + return nil + } +} + func WithEmail(email string, primary bool) UserTraitOption { return func(ut *v2.UserTrait) error { if email == "" { diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/uhttp/authcredentials.go b/vendor/github.com/conductorone/baton-sdk/pkg/uhttp/authcredentials.go new file mode 100644 index 00000000..b7b5c614 --- /dev/null +++ b/vendor/github.com/conductorone/baton-sdk/pkg/uhttp/authcredentials.go @@ -0,0 +1,157 @@ +package uhttp + +import ( + "context" + "encoding/base64" + "fmt" + "net/http" + "net/url" + + "golang.org/x/oauth2" + "golang.org/x/oauth2/clientcredentials" + "golang.org/x/oauth2/jwt" +) + +type AuthCredentials interface { + GetClient(ctx context.Context, options ...Option) (*http.Client, error) +} + +type NoAuth struct{} + +var _ AuthCredentials = (*NoAuth)(nil) + +func (n *NoAuth) GetClient(ctx context.Context, options ...Option) (*http.Client, error) { + return getHttpClient(ctx, options...) +} + +type BearerAuth struct { + Token string +} + +var _ AuthCredentials = (*BearerAuth)(nil) + +func NewBearerAuth(token string) *BearerAuth { + return &BearerAuth{ + Token: token, + } +} + +func (b *BearerAuth) GetClient(ctx context.Context, options ...Option) (*http.Client, error) { + httpClient, err := getHttpClient(ctx, options...) + if err != nil { + return nil, err + } + ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient) + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: b.Token}, + ) + httpClient = oauth2.NewClient(ctx, ts) + + return httpClient, nil +} + +type BasicAuth struct { + Username string + Password string +} + +var _ AuthCredentials = (*BasicAuth)(nil) + +func NewBasicAuth(username, password string) *BasicAuth { + return &BasicAuth{ + Username: username, + Password: password, + } +} + +func (b *BasicAuth) GetClient(ctx context.Context, options ...Option) (*http.Client, error) { + httpClient, err := getHttpClient(ctx, options...) + if err != nil { + return nil, err + } + ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient) + auth := b.Username + ":" + b.Password + token := base64.StdEncoding.EncodeToString([]byte(auth)) + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: token, TokenType: "basic"}, + ) + httpClient = oauth2.NewClient(ctx, ts) + + return httpClient, nil +} + +type OAuth2ClientCredentials struct { + cfg *clientcredentials.Config +} + +var _ AuthCredentials = (*OAuth2ClientCredentials)(nil) + +func NewOAuth2ClientCredentials(clientId, clientSecret string, tokenURL *url.URL, scopes []string) *OAuth2ClientCredentials { + return &OAuth2ClientCredentials{ + cfg: &clientcredentials.Config{ + ClientID: clientId, + ClientSecret: clientSecret, + TokenURL: tokenURL.String(), + Scopes: scopes, + }, + } +} + +func (o *OAuth2ClientCredentials) GetClient(ctx context.Context, options ...Option) (*http.Client, error) { + httpClient, err := getHttpClient(ctx, options...) + if err != nil { + return nil, err + } + ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient) + ts := o.cfg.TokenSource(ctx) + httpClient = oauth2.NewClient(ctx, ts) + + return httpClient, nil +} + +type CreateJWTConfig func(credentials []byte, scopes ...string) (*jwt.Config, error) + +type OAuth2JWT struct { + Credentials []byte + Scopes []string + CreateJWTConfig CreateJWTConfig +} + +var _ AuthCredentials = (*OAuth2JWT)(nil) + +func NewOAuth2JWT(credentials []byte, scopes []string, createfn CreateJWTConfig) *OAuth2JWT { + return &OAuth2JWT{ + Credentials: credentials, + Scopes: scopes, + CreateJWTConfig: createfn, + } +} + +func (o *OAuth2JWT) GetClient(ctx context.Context, options ...Option) (*http.Client, error) { + httpClient, err := getHttpClient(ctx, options...) + if err != nil { + return nil, err + } + + jwt, err := o.CreateJWTConfig(o.Credentials, o.Scopes...) + if err != nil { + return nil, fmt.Errorf("creating JWT config failed: %w", err) + } + + ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient) + ts := jwt.TokenSource(ctx) + httpClient = oauth2.NewClient(ctx, ts) + + return httpClient, nil +} + +func getHttpClient(ctx context.Context, options ...Option) (*http.Client, error) { + options = append(options, WithLogger(true, nil)) + + httpClient, err := NewClient(ctx, options...) + if err != nil { + return nil, fmt.Errorf("creating HTTP client failed: %w", err) + } + + return httpClient, nil +} diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/uhttp/wrapper.go b/vendor/github.com/conductorone/baton-sdk/pkg/uhttp/wrapper.go new file mode 100644 index 00000000..2b4a1e29 --- /dev/null +++ b/vendor/github.com/conductorone/baton-sdk/pkg/uhttp/wrapper.go @@ -0,0 +1,237 @@ +package uhttp + +import ( + "bytes" + "context" + "encoding/json" + "encoding/xml" + "fmt" + "io" + "net/http" + "net/url" + + v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" + "github.com/conductorone/baton-sdk/pkg/helpers" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +const ContentType = "Content-Type" + +type WrapperResponse struct { + Header http.Header + Body []byte + Status string + StatusCode int +} + +type ( + HttpClient interface { + HttpClient() *http.Client + Do(req *http.Request, options ...DoOption) (*http.Response, error) + NewRequest(ctx context.Context, method string, url *url.URL, options ...RequestOption) (*http.Request, error) + } + BaseHttpClient struct { + HttpClient *http.Client + } + + DoOption func(resp *WrapperResponse) error + RequestOption func() (io.ReadWriter, map[string]string, error) +) + +func NewBaseHttpClient(httpClient *http.Client) *BaseHttpClient { + return &BaseHttpClient{ + HttpClient: httpClient, + } +} + +func WithJSONResponse(response interface{}) DoOption { + return func(resp *WrapperResponse) error { + if !helpers.IsJSONContentType(resp.Header.Get(ContentType)) { + return fmt.Errorf("unexpected content type for json response: %s", resp.Header.Get(ContentType)) + } + return json.Unmarshal(resp.Body, response) + } +} + +type ErrorResponse interface { + Message() string +} + +func WithErrorResponse(resource ErrorResponse) DoOption { + return func(resp *WrapperResponse) error { + if resp.StatusCode < 300 { + return nil + } + + if !helpers.IsJSONContentType(resp.Header.Get(ContentType)) { + return fmt.Errorf("%v", string(resp.Body)) + } + + // Decode the JSON response body into the ErrorResponse + if err := json.Unmarshal(resp.Body, &resource); err != nil { + return status.Error(codes.Unknown, "Request failed with unknown error") + } + + // Construct a more detailed error message + errMsg := fmt.Sprintf("Request failed with status %d: %s", resp.StatusCode, resource.Message()) + + return status.Error(codes.Unknown, errMsg) + } +} + +func WithRatelimitData(resource *v2.RateLimitDescription) DoOption { + return func(resp *WrapperResponse) error { + rl, err := helpers.ExtractRateLimitData(resp.StatusCode, &resp.Header) + if err != nil { + return err + } + + resource.Limit = rl.Limit + resource.Remaining = rl.Remaining + resource.ResetAt = rl.ResetAt + resource.Status = rl.Status + + return nil + } +} + +func WithXMLResponse(response interface{}) DoOption { + return func(resp *WrapperResponse) error { + if !helpers.IsXMLContentType(resp.Header.Get(ContentType)) { + return fmt.Errorf("unexpected content type for xml response: %s", resp.Header.Get(ContentType)) + } + return xml.Unmarshal(resp.Body, response) + } +} + +func WithResponse(response interface{}) DoOption { + return func(resp *WrapperResponse) error { + if helpers.IsJSONContentType(resp.Header.Get(ContentType)) { + return WithJSONResponse(response)(resp) + } + if helpers.IsXMLContentType(resp.Header.Get(ContentType)) { + return WithXMLResponse(response)(resp) + } + + return status.Error(codes.Unknown, "unsupported content type") + } +} + +func (c *BaseHttpClient) Do(req *http.Request, options ...DoOption) (*http.Response, error) { + resp, err := c.HttpClient.Do(req) + if err != nil { + return nil, err + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + err = resp.Body.Close() + if err != nil { + return nil, err + } + + // Replace resp.Body with a no-op closer so nobody has to worry about closing the reader. + resp.Body = io.NopCloser(bytes.NewBuffer(body)) + + wresp := WrapperResponse{ + Header: resp.Header, + Status: resp.Status, + StatusCode: resp.StatusCode, + Body: body, + } + for _, option := range options { + err = option(&wresp) + if err != nil { + return resp, err + } + } + + switch resp.StatusCode { + case http.StatusTooManyRequests: + return resp, status.Error(codes.Unavailable, resp.Status) + case http.StatusNotFound: + return resp, status.Error(codes.NotFound, resp.Status) + case http.StatusUnauthorized: + return resp, status.Error(codes.Unauthenticated, resp.Status) + case http.StatusForbidden: + return resp, status.Error(codes.PermissionDenied, resp.Status) + case http.StatusNotImplemented: + return resp, status.Error(codes.Unimplemented, resp.Status) + } + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return resp, status.Error(codes.Unknown, fmt.Sprintf("unexpected status code: %d", resp.StatusCode)) + } + + return resp, err +} + +func WithHeader(key, value string) RequestOption { + return func() (io.ReadWriter, map[string]string, error) { + return nil, map[string]string{ + key: value, + }, nil + } +} + +func WithJSONBody(body interface{}) RequestOption { + return func() (io.ReadWriter, map[string]string, error) { + buffer := new(bytes.Buffer) + err := json.NewEncoder(buffer).Encode(body) + if err != nil { + return nil, nil, err + } + + _, headers, err := WithContentTypeJSONHeader()() + if err != nil { + return nil, nil, err + } + + return buffer, headers, nil + } +} + +func WithAcceptJSONHeader() RequestOption { + return WithHeader("Accept", "application/json") +} + +func WithContentTypeJSONHeader() RequestOption { + return WithHeader("Content-Type", "application/json") +} + +func WithAcceptXMLHeader() RequestOption { + return WithHeader("Accept", "application/xml") +} + +func (c *BaseHttpClient) NewRequest(ctx context.Context, method string, url *url.URL, options ...RequestOption) (*http.Request, error) { + var buffer io.ReadWriter + var headers map[string]string = make(map[string]string) + for _, option := range options { + buf, h, err := option() + if err != nil { + return nil, err + } + + if buf != nil { + buffer = buf + } + + for k, v := range h { + headers[k] = v + } + } + + req, err := http.NewRequestWithContext(ctx, method, url.String(), buffer) + if err != nil { + return nil, err + } + + for k, v := range headers { + req.Header.Set(k, v) + } + + return req, nil +} diff --git a/vendor/golang.org/x/oauth2/clientcredentials/clientcredentials.go b/vendor/golang.org/x/oauth2/clientcredentials/clientcredentials.go new file mode 100644 index 00000000..2459d069 --- /dev/null +++ b/vendor/golang.org/x/oauth2/clientcredentials/clientcredentials.go @@ -0,0 +1,124 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package clientcredentials implements the OAuth2.0 "client credentials" token flow, +// also known as the "two-legged OAuth 2.0". +// +// This should be used when the client is acting on its own behalf or when the client +// is the resource owner. It may also be used when requesting access to protected +// resources based on an authorization previously arranged with the authorization +// server. +// +// See https://tools.ietf.org/html/rfc6749#section-4.4 +package clientcredentials // import "golang.org/x/oauth2/clientcredentials" + +import ( + "context" + "fmt" + "net/http" + "net/url" + "strings" + + "golang.org/x/oauth2" + "golang.org/x/oauth2/internal" +) + +// Config describes a 2-legged OAuth2 flow, with both the +// client application information and the server's endpoint URLs. +type Config struct { + // ClientID is the application's ID. + ClientID string + + // ClientSecret is the application's secret. + ClientSecret string + + // TokenURL is the resource server's token endpoint + // URL. This is a constant specific to each server. + TokenURL string + + // Scope specifies optional requested permissions. + Scopes []string + + // EndpointParams specifies additional parameters for requests to the token endpoint. + EndpointParams url.Values + + // AuthStyle optionally specifies how the endpoint wants the + // client ID & client secret sent. The zero value means to + // auto-detect. + AuthStyle oauth2.AuthStyle + + // authStyleCache caches which auth style to use when Endpoint.AuthStyle is + // the zero value (AuthStyleAutoDetect). + authStyleCache internal.LazyAuthStyleCache +} + +// Token uses client credentials to retrieve a token. +// +// The provided context optionally controls which HTTP client is used. See the oauth2.HTTPClient variable. +func (c *Config) Token(ctx context.Context) (*oauth2.Token, error) { + return c.TokenSource(ctx).Token() +} + +// Client returns an HTTP client using the provided token. +// The token will auto-refresh as necessary. +// +// The provided context optionally controls which HTTP client +// is returned. See the oauth2.HTTPClient variable. +// +// The returned Client and its Transport should not be modified. +func (c *Config) Client(ctx context.Context) *http.Client { + return oauth2.NewClient(ctx, c.TokenSource(ctx)) +} + +// TokenSource returns a TokenSource that returns t until t expires, +// automatically refreshing it as necessary using the provided context and the +// client ID and client secret. +// +// Most users will use Config.Client instead. +func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource { + source := &tokenSource{ + ctx: ctx, + conf: c, + } + return oauth2.ReuseTokenSource(nil, source) +} + +type tokenSource struct { + ctx context.Context + conf *Config +} + +// Token refreshes the token by using a new client credentials request. +// tokens received this way do not include a refresh token +func (c *tokenSource) Token() (*oauth2.Token, error) { + v := url.Values{ + "grant_type": {"client_credentials"}, + } + if len(c.conf.Scopes) > 0 { + v.Set("scope", strings.Join(c.conf.Scopes, " ")) + } + for k, p := range c.conf.EndpointParams { + // Allow grant_type to be overridden to allow interoperability with + // non-compliant implementations. + if _, ok := v[k]; ok && k != "grant_type" { + return nil, fmt.Errorf("oauth2: cannot overwrite parameter %q", k) + } + v[k] = p + } + + tk, err := internal.RetrieveToken(c.ctx, c.conf.ClientID, c.conf.ClientSecret, c.conf.TokenURL, v, internal.AuthStyle(c.conf.AuthStyle), c.conf.authStyleCache.Get()) + if err != nil { + if rErr, ok := err.(*internal.RetrieveError); ok { + return nil, (*oauth2.RetrieveError)(rErr) + } + return nil, err + } + t := &oauth2.Token{ + AccessToken: tk.AccessToken, + TokenType: tk.TokenType, + RefreshToken: tk.RefreshToken, + Expiry: tk.Expiry, + } + return t.WithExtra(tk.Raw), nil +} diff --git a/vendor/golang.org/x/oauth2/jws/jws.go b/vendor/golang.org/x/oauth2/jws/jws.go new file mode 100644 index 00000000..95015648 --- /dev/null +++ b/vendor/golang.org/x/oauth2/jws/jws.go @@ -0,0 +1,182 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package jws provides a partial implementation +// of JSON Web Signature encoding and decoding. +// It exists to support the golang.org/x/oauth2 package. +// +// See RFC 7515. +// +// Deprecated: this package is not intended for public use and might be +// removed in the future. It exists for internal use only. +// Please switch to another JWS package or copy this package into your own +// source tree. +package jws // import "golang.org/x/oauth2/jws" + +import ( + "bytes" + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "strings" + "time" +) + +// ClaimSet contains information about the JWT signature including the +// permissions being requested (scopes), the target of the token, the issuer, +// the time the token was issued, and the lifetime of the token. +type ClaimSet struct { + Iss string `json:"iss"` // email address of the client_id of the application making the access token request + Scope string `json:"scope,omitempty"` // space-delimited list of the permissions the application requests + Aud string `json:"aud"` // descriptor of the intended target of the assertion (Optional). + Exp int64 `json:"exp"` // the expiration time of the assertion (seconds since Unix epoch) + Iat int64 `json:"iat"` // the time the assertion was issued (seconds since Unix epoch) + Typ string `json:"typ,omitempty"` // token type (Optional). + + // Email for which the application is requesting delegated access (Optional). + Sub string `json:"sub,omitempty"` + + // The old name of Sub. Client keeps setting Prn to be + // complaint with legacy OAuth 2.0 providers. (Optional) + Prn string `json:"prn,omitempty"` + + // See http://tools.ietf.org/html/draft-jones-json-web-token-10#section-4.3 + // This array is marshalled using custom code (see (c *ClaimSet) encode()). + PrivateClaims map[string]interface{} `json:"-"` +} + +func (c *ClaimSet) encode() (string, error) { + // Reverting time back for machines whose time is not perfectly in sync. + // If client machine's time is in the future according + // to Google servers, an access token will not be issued. + now := time.Now().Add(-10 * time.Second) + if c.Iat == 0 { + c.Iat = now.Unix() + } + if c.Exp == 0 { + c.Exp = now.Add(time.Hour).Unix() + } + if c.Exp < c.Iat { + return "", fmt.Errorf("jws: invalid Exp = %v; must be later than Iat = %v", c.Exp, c.Iat) + } + + b, err := json.Marshal(c) + if err != nil { + return "", err + } + + if len(c.PrivateClaims) == 0 { + return base64.RawURLEncoding.EncodeToString(b), nil + } + + // Marshal private claim set and then append it to b. + prv, err := json.Marshal(c.PrivateClaims) + if err != nil { + return "", fmt.Errorf("jws: invalid map of private claims %v", c.PrivateClaims) + } + + // Concatenate public and private claim JSON objects. + if !bytes.HasSuffix(b, []byte{'}'}) { + return "", fmt.Errorf("jws: invalid JSON %s", b) + } + if !bytes.HasPrefix(prv, []byte{'{'}) { + return "", fmt.Errorf("jws: invalid JSON %s", prv) + } + b[len(b)-1] = ',' // Replace closing curly brace with a comma. + b = append(b, prv[1:]...) // Append private claims. + return base64.RawURLEncoding.EncodeToString(b), nil +} + +// Header represents the header for the signed JWS payloads. +type Header struct { + // The algorithm used for signature. + Algorithm string `json:"alg"` + + // Represents the token type. + Typ string `json:"typ"` + + // The optional hint of which key is being used. + KeyID string `json:"kid,omitempty"` +} + +func (h *Header) encode() (string, error) { + b, err := json.Marshal(h) + if err != nil { + return "", err + } + return base64.RawURLEncoding.EncodeToString(b), nil +} + +// Decode decodes a claim set from a JWS payload. +func Decode(payload string) (*ClaimSet, error) { + // decode returned id token to get expiry + s := strings.Split(payload, ".") + if len(s) < 2 { + // TODO(jbd): Provide more context about the error. + return nil, errors.New("jws: invalid token received") + } + decoded, err := base64.RawURLEncoding.DecodeString(s[1]) + if err != nil { + return nil, err + } + c := &ClaimSet{} + err = json.NewDecoder(bytes.NewBuffer(decoded)).Decode(c) + return c, err +} + +// Signer returns a signature for the given data. +type Signer func(data []byte) (sig []byte, err error) + +// EncodeWithSigner encodes a header and claim set with the provided signer. +func EncodeWithSigner(header *Header, c *ClaimSet, sg Signer) (string, error) { + head, err := header.encode() + if err != nil { + return "", err + } + cs, err := c.encode() + if err != nil { + return "", err + } + ss := fmt.Sprintf("%s.%s", head, cs) + sig, err := sg([]byte(ss)) + if err != nil { + return "", err + } + return fmt.Sprintf("%s.%s", ss, base64.RawURLEncoding.EncodeToString(sig)), nil +} + +// Encode encodes a signed JWS with provided header and claim set. +// This invokes EncodeWithSigner using crypto/rsa.SignPKCS1v15 with the given RSA private key. +func Encode(header *Header, c *ClaimSet, key *rsa.PrivateKey) (string, error) { + sg := func(data []byte) (sig []byte, err error) { + h := sha256.New() + h.Write(data) + return rsa.SignPKCS1v15(rand.Reader, key, crypto.SHA256, h.Sum(nil)) + } + return EncodeWithSigner(header, c, sg) +} + +// Verify tests whether the provided JWT token's signature was produced by the private key +// associated with the supplied public key. +func Verify(token string, key *rsa.PublicKey) error { + parts := strings.Split(token, ".") + if len(parts) != 3 { + return errors.New("jws: invalid token received, token must have 3 parts") + } + + signedContent := parts[0] + "." + parts[1] + signatureString, err := base64.RawURLEncoding.DecodeString(parts[2]) + if err != nil { + return err + } + + h := sha256.New() + h.Write([]byte(signedContent)) + return rsa.VerifyPKCS1v15(key, crypto.SHA256, h.Sum(nil), signatureString) +} diff --git a/vendor/golang.org/x/oauth2/jwt/jwt.go b/vendor/golang.org/x/oauth2/jwt/jwt.go new file mode 100644 index 00000000..b2bf1829 --- /dev/null +++ b/vendor/golang.org/x/oauth2/jwt/jwt.go @@ -0,0 +1,185 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package jwt implements the OAuth 2.0 JSON Web Token flow, commonly +// known as "two-legged OAuth 2.0". +// +// See: https://tools.ietf.org/html/draft-ietf-oauth-jwt-bearer-12 +package jwt + +import ( + "context" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "strings" + "time" + + "golang.org/x/oauth2" + "golang.org/x/oauth2/internal" + "golang.org/x/oauth2/jws" +) + +var ( + defaultGrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer" + defaultHeader = &jws.Header{Algorithm: "RS256", Typ: "JWT"} +) + +// Config is the configuration for using JWT to fetch tokens, +// commonly known as "two-legged OAuth 2.0". +type Config struct { + // Email is the OAuth client identifier used when communicating with + // the configured OAuth provider. + Email string + + // PrivateKey contains the contents of an RSA private key or the + // contents of a PEM file that contains a private key. The provided + // private key is used to sign JWT payloads. + // PEM containers with a passphrase are not supported. + // Use the following command to convert a PKCS 12 file into a PEM. + // + // $ openssl pkcs12 -in key.p12 -out key.pem -nodes + // + PrivateKey []byte + + // PrivateKeyID contains an optional hint indicating which key is being + // used. + PrivateKeyID string + + // Subject is the optional user to impersonate. + Subject string + + // Scopes optionally specifies a list of requested permission scopes. + Scopes []string + + // TokenURL is the endpoint required to complete the 2-legged JWT flow. + TokenURL string + + // Expires optionally specifies how long the token is valid for. + Expires time.Duration + + // Audience optionally specifies the intended audience of the + // request. If empty, the value of TokenURL is used as the + // intended audience. + Audience string + + // PrivateClaims optionally specifies custom private claims in the JWT. + // See http://tools.ietf.org/html/draft-jones-json-web-token-10#section-4.3 + PrivateClaims map[string]interface{} + + // UseIDToken optionally specifies whether ID token should be used instead + // of access token when the server returns both. + UseIDToken bool +} + +// TokenSource returns a JWT TokenSource using the configuration +// in c and the HTTP client from the provided context. +func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource { + return oauth2.ReuseTokenSource(nil, jwtSource{ctx, c}) +} + +// Client returns an HTTP client wrapping the context's +// HTTP transport and adding Authorization headers with tokens +// obtained from c. +// +// The returned client and its Transport should not be modified. +func (c *Config) Client(ctx context.Context) *http.Client { + return oauth2.NewClient(ctx, c.TokenSource(ctx)) +} + +// jwtSource is a source that always does a signed JWT request for a token. +// It should typically be wrapped with a reuseTokenSource. +type jwtSource struct { + ctx context.Context + conf *Config +} + +func (js jwtSource) Token() (*oauth2.Token, error) { + pk, err := internal.ParseKey(js.conf.PrivateKey) + if err != nil { + return nil, err + } + hc := oauth2.NewClient(js.ctx, nil) + claimSet := &jws.ClaimSet{ + Iss: js.conf.Email, + Scope: strings.Join(js.conf.Scopes, " "), + Aud: js.conf.TokenURL, + PrivateClaims: js.conf.PrivateClaims, + } + if subject := js.conf.Subject; subject != "" { + claimSet.Sub = subject + // prn is the old name of sub. Keep setting it + // to be compatible with legacy OAuth 2.0 providers. + claimSet.Prn = subject + } + if t := js.conf.Expires; t > 0 { + claimSet.Exp = time.Now().Add(t).Unix() + } + if aud := js.conf.Audience; aud != "" { + claimSet.Aud = aud + } + h := *defaultHeader + h.KeyID = js.conf.PrivateKeyID + payload, err := jws.Encode(&h, claimSet, pk) + if err != nil { + return nil, err + } + v := url.Values{} + v.Set("grant_type", defaultGrantType) + v.Set("assertion", payload) + resp, err := hc.PostForm(js.conf.TokenURL, v) + if err != nil { + return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err) + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20)) + if err != nil { + return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err) + } + if c := resp.StatusCode; c < 200 || c > 299 { + return nil, &oauth2.RetrieveError{ + Response: resp, + Body: body, + } + } + // tokenRes is the JSON response body. + var tokenRes struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + IDToken string `json:"id_token"` + ExpiresIn int64 `json:"expires_in"` // relative seconds from now + } + if err := json.Unmarshal(body, &tokenRes); err != nil { + return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err) + } + token := &oauth2.Token{ + AccessToken: tokenRes.AccessToken, + TokenType: tokenRes.TokenType, + } + raw := make(map[string]interface{}) + json.Unmarshal(body, &raw) // no error checks for optional fields + token = token.WithExtra(raw) + + if secs := tokenRes.ExpiresIn; secs > 0 { + token.Expiry = time.Now().Add(time.Duration(secs) * time.Second) + } + if v := tokenRes.IDToken; v != "" { + // decode returned id token to get expiry + claimSet, err := jws.Decode(v) + if err != nil { + return nil, fmt.Errorf("oauth2: error decoding JWT token: %v", err) + } + token.Expiry = time.Unix(claimSet.Exp, 0) + } + if js.conf.UseIDToken { + if tokenRes.IDToken == "" { + return nil, fmt.Errorf("oauth2: response doesn't have JWT token") + } + token.AccessToken = tokenRes.IDToken + } + return token, nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index f22dc33b..6350828b 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -135,7 +135,7 @@ github.com/aws/smithy-go/waiter # github.com/benbjohnson/clock v1.3.5 ## explicit; go 1.15 github.com/benbjohnson/clock -# github.com/conductorone/baton-sdk v0.1.22 +# github.com/conductorone/baton-sdk v0.1.33 ## explicit; go 1.20 github.com/conductorone/baton-sdk/internal/connector github.com/conductorone/baton-sdk/pb/c1/c1z/v1 @@ -157,6 +157,7 @@ github.com/conductorone/baton-sdk/pkg/dotc1z github.com/conductorone/baton-sdk/pkg/dotc1z/manager github.com/conductorone/baton-sdk/pkg/dotc1z/manager/local github.com/conductorone/baton-sdk/pkg/dotc1z/manager/s3 +github.com/conductorone/baton-sdk/pkg/helpers github.com/conductorone/baton-sdk/pkg/logging github.com/conductorone/baton-sdk/pkg/pagination github.com/conductorone/baton-sdk/pkg/provisioner @@ -429,7 +430,10 @@ golang.org/x/net/trace # golang.org/x/oauth2 v0.14.0 ## explicit; go 1.18 golang.org/x/oauth2 +golang.org/x/oauth2/clientcredentials golang.org/x/oauth2/internal +golang.org/x/oauth2/jws +golang.org/x/oauth2/jwt # golang.org/x/sync v0.5.0 ## explicit; go 1.18 golang.org/x/sync/semaphore