Merge branch 'master' into bugfix/de-errors
commit
a6280c0cfa
|
@ -1,5 +1,5 @@
|
|||
[bumpversion]
|
||||
current_version = 1.4.2.1
|
||||
current_version = 1.4.2.3
|
||||
files = README.md server/swagger.json
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\.(?P<release>\d+)
|
||||
serialize = {major}.{minor}.{patch}.{release}
|
||||
|
|
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -5,9 +5,17 @@
|
|||
### UI Improvements
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
1. [#2911](https://github.com/influxdata/chronograf/pull/2911): Fix Heroku OAuth
|
||||
1. [#2953](https://github.com/influxdata/chronograf/pull/2953): Fix error reporting in DataExplorer
|
||||
1. [#2947](https://github.com/influxdata/chronograf/pull/2947): Fix Okta oauth2 provider support
|
||||
1. [#2866](https://github.com/influxdata/chronograf/pull/2866): Change hover text on delete mappings confirmation button to 'Delete'
|
||||
|
||||
## v1.4.2.3 [2018-03-08]
|
||||
|
||||
## v1.4.2.2 [2018-03-07]
|
||||
### Bug Fixes
|
||||
1. [#2933](https://github.com/influxdata/chronograf/pull/2933): Include url in Kapacitor connection creation requests
|
||||
|
||||
|
||||
## v1.4.2.1 [2018-02-28]
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ We really like to receive feature requests, as it helps us prioritize our work.
|
|||
|
||||
Contributing to the source code
|
||||
-------------------------------
|
||||
Chronograf is built using Go for its API backend and serving the front-end assets. The front-end visualization is built with React and uses Yarn for package management. The assumption is that all your Go development are done in `$GOPATH/src`. `GOPATH` can be any directory under which Chronograf and all its dependencies will be cloned. For full details on the project structure, follow along below.
|
||||
Chronograf is built using Go for its API backend and serving the front-end assets, and uses Dep for dependency management. The front-end visualization is built with React (JavaScript) and uses Yarn for dependency management. The assumption is that all your Go development are done in `$GOPATH/src`. `GOPATH` can be any directory under which Chronograf and all its dependencies will be cloned. For full details on the project structure, follow along below.
|
||||
|
||||
Submitting a pull request
|
||||
-------------------------
|
||||
|
@ -43,9 +43,13 @@ Signing the CLA
|
|||
If you are going to be contributing back to Chronograf please take a second to sign our CLA, which can be found
|
||||
[on our website](https://influxdata.com/community/cla/).
|
||||
|
||||
Installing Yarn
|
||||
Installing & Using Yarn
|
||||
--------------
|
||||
You'll need to install Yarn to manage the JavaScript modules that the front-end uses. This varies depending on what platform you're developing on, but you should be able to find an installer on [the Yarn installation page](https://yarnpkg.com/en/docs/install).
|
||||
You'll need to install Yarn to manage the frontend (JavaScript) dependencies.
|
||||
|
||||
* [Install Yarn](https://yarnpkg.com/en/docs/install)
|
||||
|
||||
To add a dependency via Yarn, for example, run `yarn add <dependency>` from within the `/chronograf/ui` directory.
|
||||
|
||||
Installing Go
|
||||
-------------
|
||||
|
@ -62,13 +66,13 @@ running the following:
|
|||
gvm use go1.7.5 --default
|
||||
```
|
||||
|
||||
Installing GDM
|
||||
Installing & Using Dep
|
||||
--------------
|
||||
Chronograf uses [gdm](https://github.com/sparrc/gdm) to manage dependencies. Install it by running the following:
|
||||
You'll need to install Dep to manage the backend (Go) dependencies.
|
||||
|
||||
```bash
|
||||
go get github.com/sparrc/gdm
|
||||
```
|
||||
* [Install Dep](https://github.com/golang/dep)
|
||||
|
||||
To add a dependency via Dep, for example, run `dep ensure -add <dependency>` from within the `/chronograf` directory. _Note that as of this writing, `dep ensure` will modify many extraneous vendor files, so you'll need to run `dep prune` to clean this up before committing your changes. Apparently, the next version of `dep` will take care of this step for you._
|
||||
|
||||
Revision Control Systems
|
||||
------------------------
|
||||
|
|
|
@ -39,34 +39,7 @@
|
|||
|
||||
[[projects]]
|
||||
name = "github.com/gogo/protobuf"
|
||||
packages = [
|
||||
"gogoproto",
|
||||
"jsonpb",
|
||||
"plugin/compare",
|
||||
"plugin/defaultcheck",
|
||||
"plugin/description",
|
||||
"plugin/embedcheck",
|
||||
"plugin/enumstringer",
|
||||
"plugin/equal",
|
||||
"plugin/face",
|
||||
"plugin/gostring",
|
||||
"plugin/marshalto",
|
||||
"plugin/oneofcheck",
|
||||
"plugin/populate",
|
||||
"plugin/size",
|
||||
"plugin/stringer",
|
||||
"plugin/testgen",
|
||||
"plugin/union",
|
||||
"plugin/unmarshal",
|
||||
"proto",
|
||||
"protoc-gen-gogo",
|
||||
"protoc-gen-gogo/descriptor",
|
||||
"protoc-gen-gogo/generator",
|
||||
"protoc-gen-gogo/grpc",
|
||||
"protoc-gen-gogo/plugin",
|
||||
"vanity",
|
||||
"vanity/command"
|
||||
]
|
||||
packages = ["gogoproto","jsonpb","plugin/compare","plugin/defaultcheck","plugin/description","plugin/embedcheck","plugin/enumstringer","plugin/equal","plugin/face","plugin/gostring","plugin/marshalto","plugin/oneofcheck","plugin/populate","plugin/size","plugin/stringer","plugin/testgen","plugin/union","plugin/unmarshal","proto","protoc-gen-gogo","protoc-gen-gogo/descriptor","protoc-gen-gogo/generator","protoc-gen-gogo/grpc","protoc-gen-gogo/plugin","vanity","vanity/command"]
|
||||
revision = "6abcf94fd4c97dcb423fdafd42fe9f96ca7e421b"
|
||||
|
||||
[[projects]]
|
||||
|
@ -77,13 +50,7 @@
|
|||
|
||||
[[projects]]
|
||||
name = "github.com/google/go-cmp"
|
||||
packages = [
|
||||
"cmp",
|
||||
"cmp/cmpopts",
|
||||
"cmp/internal/diff",
|
||||
"cmp/internal/function",
|
||||
"cmp/internal/value"
|
||||
]
|
||||
packages = ["cmp","cmp/cmpopts","cmp/internal/diff","cmp/internal/function","cmp/internal/value"]
|
||||
revision = "8099a9787ce5dc5984ed879a3bda47dc730a8e97"
|
||||
version = "v0.1.0"
|
||||
|
||||
|
@ -100,28 +67,13 @@
|
|||
|
||||
[[projects]]
|
||||
name = "github.com/influxdata/influxdb"
|
||||
packages = [
|
||||
"influxql",
|
||||
"influxql/internal",
|
||||
"influxql/neldermead",
|
||||
"models",
|
||||
"pkg/escape"
|
||||
]
|
||||
packages = ["influxql","influxql/internal","influxql/neldermead","models","pkg/escape"]
|
||||
revision = "cd9363b52cac452113b95554d98a6be51beda24e"
|
||||
version = "v1.1.5"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/influxdata/kapacitor"
|
||||
packages = [
|
||||
"client/v1",
|
||||
"pipeline",
|
||||
"pipeline/tick",
|
||||
"services/k8s/client",
|
||||
"tick",
|
||||
"tick/ast",
|
||||
"tick/stateful",
|
||||
"udf/agent"
|
||||
]
|
||||
packages = ["client/v1","pipeline","pipeline/tick","services/k8s/client","tick","tick/ast","tick/stateful","udf/agent"]
|
||||
revision = "6de30070b39afde111fea5e041281126fe8aae31"
|
||||
|
||||
[[projects]]
|
||||
|
@ -163,21 +115,13 @@
|
|||
|
||||
[[projects]]
|
||||
name = "golang.org/x/net"
|
||||
packages = [
|
||||
"context",
|
||||
"context/ctxhttp"
|
||||
]
|
||||
packages = ["context","context/ctxhttp"]
|
||||
revision = "749a502dd1eaf3e5bfd4f8956748c502357c0bbe"
|
||||
|
||||
[[projects]]
|
||||
name = "golang.org/x/oauth2"
|
||||
packages = [
|
||||
".",
|
||||
"github",
|
||||
"heroku",
|
||||
"internal"
|
||||
]
|
||||
revision = "1e695b1c8febf17aad3bfa7bf0a819ef94b98ad5"
|
||||
packages = [".","github","heroku","internal"]
|
||||
revision = "2f32c3ac0fa4fb807a0fcefb0b6f2468a0d99bd0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
|
@ -187,31 +131,18 @@
|
|||
|
||||
[[projects]]
|
||||
name = "google.golang.org/api"
|
||||
packages = [
|
||||
"gensupport",
|
||||
"googleapi",
|
||||
"googleapi/internal/uritemplates",
|
||||
"oauth2/v2"
|
||||
]
|
||||
packages = ["gensupport","googleapi","googleapi/internal/uritemplates","oauth2/v2"]
|
||||
revision = "bc20c61134e1d25265dd60049f5735381e79b631"
|
||||
|
||||
[[projects]]
|
||||
name = "google.golang.org/appengine"
|
||||
packages = [
|
||||
"internal",
|
||||
"internal/base",
|
||||
"internal/datastore",
|
||||
"internal/log",
|
||||
"internal/remote_api",
|
||||
"internal/urlfetch",
|
||||
"urlfetch"
|
||||
]
|
||||
packages = ["internal","internal/base","internal/datastore","internal/log","internal/remote_api","internal/urlfetch","urlfetch"]
|
||||
revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a"
|
||||
version = "v1.0.0"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "11df631364d11bc05c8f71af1aa735360b5a40a793d32d47d1f1d8c694a55f6f"
|
||||
inputs-digest = "a4df1b0953349e64a89581f4b83ac3a2f40e17681e19f8de3cbf828b6375a3ba"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
|
|
@ -62,7 +62,7 @@ required = ["github.com/kevinburke/go-bindata","github.com/gogo/protobuf/proto",
|
|||
|
||||
[[constraint]]
|
||||
name = "golang.org/x/oauth2"
|
||||
revision = "1e695b1c8febf17aad3bfa7bf0a819ef94b98ad5"
|
||||
revision = "2f32c3ac0fa4fb807a0fcefb0b6f2468a0d99bd0"
|
||||
|
||||
[[constraint]]
|
||||
name = "google.golang.org/api"
|
||||
|
|
|
@ -136,7 +136,7 @@ option.
|
|||
## Versions
|
||||
|
||||
The most recent version of Chronograf is
|
||||
[v1.4.2.1](https://www.influxdata.com/downloads/).
|
||||
[v1.4.2.3](https://www.influxdata.com/downloads/).
|
||||
|
||||
Spotted a bug or have a feature request? Please open
|
||||
[an issue](https://github.com/influxdata/chronograf/issues/new)!
|
||||
|
@ -178,7 +178,7 @@ By default, chronograf runs on port `8888`.
|
|||
To get started right away with Docker, you can pull down our latest release:
|
||||
|
||||
```sh
|
||||
docker pull chronograf:1.4.2.1
|
||||
docker pull chronograf:1.4.2.3
|
||||
```
|
||||
|
||||
### From Source
|
||||
|
|
|
@ -1 +1 @@
|
|||
**We've moved our documentation!** Check out the latest [authentication content](https://docs.influxdata.com/chronograf/latest/administration/security-best-practices/#chronograf-with-oauth-2-0-authentication) on InfluxData's [main docs site](https://docs.influxdata.com/chronograf/latest/).
|
||||
**We've moved our documentation!** Check out the latest [authentication content](https://docs.influxdata.com/chronograf/latest/administration/managing-security/#oauth-2-0-providers-with-jwt-tokens) on InfluxData's [main docs site](https://docs.influxdata.com/chronograf/latest/).
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package oauth2
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/http/httptest"
|
||||
|
@ -13,15 +14,24 @@ import (
|
|||
|
||||
var testTime = time.Date(1985, time.October, 25, 18, 0, 0, 0, time.UTC)
|
||||
|
||||
type mockCallbackResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
}
|
||||
|
||||
// setupMuxTest produces an http.Client and an httptest.Server configured to
|
||||
// use a particular http.Handler selected from a AuthMux. As this selection is
|
||||
// done during the setup process, this configuration is performed by providing
|
||||
// a function, and returning the desired handler. Cleanup is still the
|
||||
// responsibility of the test writer, so the httptest.Server's Close() method
|
||||
// should be deferred.
|
||||
func setupMuxTest(selector func(*AuthMux) http.Handler) (*http.Client, *httptest.Server, *httptest.Server) {
|
||||
func setupMuxTest(response interface{}, selector func(*AuthMux) http.Handler) (*http.Client, *httptest.Server, *httptest.Server) {
|
||||
provider := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.Header().Set("content-type", "application/json")
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
|
||||
body, _ := json.Marshal(response)
|
||||
|
||||
rw.Write(body)
|
||||
}))
|
||||
|
||||
now := func() time.Time {
|
||||
|
@ -63,7 +73,9 @@ func teardownMuxTest(hc *http.Client, backend *httptest.Server, provider *httpte
|
|||
func Test_AuthMux_Logout_DeletesSessionCookie(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
hc, ts, prov := setupMuxTest(func(j *AuthMux) http.Handler {
|
||||
var response interface{}
|
||||
|
||||
hc, ts, prov := setupMuxTest(response, func(j *AuthMux) http.Handler {
|
||||
return j.Logout()
|
||||
})
|
||||
defer teardownMuxTest(hc, ts, prov)
|
||||
|
@ -100,7 +112,9 @@ func Test_AuthMux_Logout_DeletesSessionCookie(t *testing.T) {
|
|||
func Test_AuthMux_Login_RedirectsToCorrectURL(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
hc, ts, prov := setupMuxTest(func(j *AuthMux) http.Handler {
|
||||
var response interface{}
|
||||
|
||||
hc, ts, prov := setupMuxTest(response, func(j *AuthMux) http.Handler {
|
||||
return j.Login() // Use Login handler for httptest server.
|
||||
})
|
||||
defer teardownMuxTest(hc, ts, prov)
|
||||
|
@ -126,7 +140,8 @@ func Test_AuthMux_Login_RedirectsToCorrectURL(t *testing.T) {
|
|||
}
|
||||
|
||||
func Test_AuthMux_Callback_SetsCookie(t *testing.T) {
|
||||
hc, ts, prov := setupMuxTest(func(j *AuthMux) http.Handler {
|
||||
response := mockCallbackResponse{AccessToken: "123"}
|
||||
hc, ts, prov := setupMuxTest(response, func(j *AuthMux) http.Handler {
|
||||
return j.Callback()
|
||||
})
|
||||
defer teardownMuxTest(hc, ts, prov)
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"info": {
|
||||
"title": "Chronograf",
|
||||
"description": "API endpoints for Chronograf",
|
||||
"version": "1.4.2.1"
|
||||
"version": "1.4.2.3"
|
||||
},
|
||||
"schemes": ["http"],
|
||||
"basePath": "/chronograf/v1",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "chronograf-ui",
|
||||
"version": "1.4.2-1",
|
||||
"version": "1.4.2-3",
|
||||
"private": false,
|
||||
"license": "AGPL-3.0",
|
||||
"description": "",
|
||||
|
|
|
@ -111,6 +111,7 @@ class OrganizationsTableRow extends Component {
|
|||
onConfirm={this.handleDeleteOrg}
|
||||
onClickOutside={this.handleDismissDeleteConfirmation}
|
||||
confirmLeft={true}
|
||||
confirmTitle="Delete"
|
||||
/>
|
||||
: <OrganizationsTableRowDeleteButton
|
||||
organization={organization}
|
||||
|
|
|
@ -109,6 +109,7 @@ class ProvidersTableRow extends Component {
|
|||
onCancel={this.handleDismissDeleteConfirmation}
|
||||
onConfirm={this.handleDeleteMap}
|
||||
onClickOutside={this.handleDismissDeleteConfirmation}
|
||||
confirmTitle="Delete"
|
||||
/>
|
||||
: <button
|
||||
className="btn btn-sm btn-default btn-square"
|
||||
|
|
|
@ -60,8 +60,8 @@ class KapacitorPage extends Component {
|
|||
})
|
||||
}
|
||||
|
||||
handleChangeUrl = ({value}) => {
|
||||
this.setState({kapacitor: {...this.state.kapacitor, url: value}})
|
||||
handleChangeUrl = e => {
|
||||
this.setState({kapacitor: {...this.state.kapacitor, url: e.target.value}})
|
||||
}
|
||||
|
||||
handleSubmit = e => {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, {Component, PropTypes} from 'react'
|
||||
import React, {Component} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import OnClickOutside from 'shared/components/OnClickOutside'
|
||||
|
||||
|
|
|
@ -1,86 +0,0 @@
|
|||
import React, {PropTypes, Component} from 'react'
|
||||
import classnames from 'classnames'
|
||||
|
||||
import OnClickOutside from 'shared/components/OnClickOutside'
|
||||
|
||||
class ConfirmButtons extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
handleConfirm = item => () => {
|
||||
this.props.onConfirm(item)
|
||||
}
|
||||
|
||||
handleCancel = item => () => {
|
||||
this.props.onCancel(item)
|
||||
}
|
||||
|
||||
handleClickOutside = () => {
|
||||
this.props.onClickOutside(this.props.item)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {item, buttonSize, isDisabled, confirmLeft} = this.props
|
||||
|
||||
return confirmLeft
|
||||
? <div className="confirm-buttons">
|
||||
<button
|
||||
className={classnames('btn btn-success btn-square', {
|
||||
[buttonSize]: buttonSize,
|
||||
})}
|
||||
disabled={isDisabled}
|
||||
title={isDisabled ? 'Cannot Save' : 'Save'}
|
||||
onClick={this.handleConfirm(item)}
|
||||
>
|
||||
<span className="icon checkmark" />
|
||||
</button>
|
||||
<button
|
||||
className={classnames('btn btn-info btn-square', {
|
||||
[buttonSize]: buttonSize,
|
||||
})}
|
||||
onClick={this.handleCancel(item)}
|
||||
>
|
||||
<span className="icon remove" />
|
||||
</button>
|
||||
</div>
|
||||
: <div className="confirm-buttons">
|
||||
<button
|
||||
className={classnames('btn btn-info btn-square', {
|
||||
[buttonSize]: buttonSize,
|
||||
})}
|
||||
onClick={this.handleCancel(item)}
|
||||
>
|
||||
<span className="icon remove" />
|
||||
</button>
|
||||
<button
|
||||
className={classnames('btn btn-success btn-square', {
|
||||
[buttonSize]: buttonSize,
|
||||
})}
|
||||
disabled={isDisabled}
|
||||
title={isDisabled ? 'Cannot Save' : 'Save'}
|
||||
onClick={this.handleConfirm(item)}
|
||||
>
|
||||
<span className="icon checkmark" />
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
const {func, oneOfType, shape, string, bool} = PropTypes
|
||||
|
||||
ConfirmButtons.propTypes = {
|
||||
onConfirm: func.isRequired,
|
||||
item: oneOfType([shape(), string]),
|
||||
onCancel: func.isRequired,
|
||||
buttonSize: string,
|
||||
isDisabled: bool,
|
||||
onClickOutside: func,
|
||||
confirmLeft: bool,
|
||||
}
|
||||
|
||||
ConfirmButtons.defaultProps = {
|
||||
buttonSize: 'btn-sm',
|
||||
onClickOutside: () => {},
|
||||
}
|
||||
export default OnClickOutside(ConfirmButtons)
|
|
@ -0,0 +1,120 @@
|
|||
import React, {PureComponent, SFC} from 'react'
|
||||
import classnames from 'classnames'
|
||||
|
||||
import OnClickOutside from 'src/shared/components/OnClickOutside'
|
||||
|
||||
type Item = Object | string
|
||||
|
||||
interface ConfirmProps {
|
||||
buttonSize: string
|
||||
isDisabled: boolean
|
||||
onConfirm: () => void
|
||||
icon: string
|
||||
title: string
|
||||
}
|
||||
|
||||
interface CancelProps {
|
||||
buttonSize: string
|
||||
onCancel: () => void
|
||||
icon: string
|
||||
}
|
||||
interface ConfirmButtonsProps {
|
||||
onConfirm: (item: Item) => void
|
||||
item: Item
|
||||
onCancel: (item: Item) => void
|
||||
buttonSize?: string
|
||||
isDisabled?: boolean
|
||||
onClickOutside?: (item: Item) => void
|
||||
confirmLeft?: boolean
|
||||
confirmTitle?: string
|
||||
}
|
||||
|
||||
export const Confirm: SFC<ConfirmProps> = ({
|
||||
buttonSize,
|
||||
isDisabled,
|
||||
onConfirm,
|
||||
icon,
|
||||
title,
|
||||
}) =>
|
||||
<button
|
||||
data-test="confirm"
|
||||
className={classnames('btn btn-success btn-square', {
|
||||
[buttonSize]: buttonSize,
|
||||
})}
|
||||
disabled={isDisabled}
|
||||
title={isDisabled ? `Cannot ${title}` : title}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
<span className={icon} />
|
||||
</button>
|
||||
|
||||
export const Cancel: SFC<CancelProps> = ({buttonSize, onCancel, icon}) =>
|
||||
<button
|
||||
data-test="cancel"
|
||||
className={classnames('btn btn-info btn-square', {
|
||||
[buttonSize]: buttonSize,
|
||||
})}
|
||||
onClick={onCancel}
|
||||
>
|
||||
<span className={icon} />
|
||||
</button>
|
||||
|
||||
class ConfirmButtons extends PureComponent<ConfirmButtonsProps, {}> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
public static defaultProps: Partial<ConfirmButtonsProps> = {
|
||||
buttonSize: 'btn-sm',
|
||||
onClickOutside: () => {},
|
||||
confirmTitle: 'Save',
|
||||
}
|
||||
|
||||
handleConfirm = item => () => {
|
||||
this.props.onConfirm(item)
|
||||
}
|
||||
|
||||
handleCancel = item => () => {
|
||||
this.props.onCancel(item)
|
||||
}
|
||||
|
||||
handleClickOutside = () => {
|
||||
this.props.onClickOutside(this.props.item)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {item, buttonSize, isDisabled, confirmLeft, confirmTitle} = this.props
|
||||
|
||||
return confirmLeft
|
||||
? <div className="confirm-buttons">
|
||||
<Confirm
|
||||
buttonSize={buttonSize}
|
||||
isDisabled={isDisabled}
|
||||
onConfirm={this.handleConfirm(item)}
|
||||
icon="icon checkmark"
|
||||
title={confirmTitle}
|
||||
/>
|
||||
<Cancel
|
||||
buttonSize={buttonSize}
|
||||
onCancel={this.handleCancel(item)}
|
||||
icon="icon remove"
|
||||
/>
|
||||
</div>
|
||||
: <div className="confirm-buttons">
|
||||
<Cancel
|
||||
buttonSize={buttonSize}
|
||||
onCancel={this.handleCancel(item)}
|
||||
icon="icon remove"
|
||||
/>
|
||||
<Confirm
|
||||
buttonSize={buttonSize}
|
||||
isDisabled={isDisabled}
|
||||
onConfirm={this.handleConfirm(item)}
|
||||
icon="icon checkmark"
|
||||
title={confirmTitle}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
export default OnClickOutside(ConfirmButtons)
|
|
@ -0,0 +1,118 @@
|
|||
import React from 'react'
|
||||
import ConfirmButtons, {
|
||||
Confirm,
|
||||
Cancel,
|
||||
} from 'src/shared/components/ConfirmButtons'
|
||||
|
||||
import {shallow} from 'enzyme'
|
||||
|
||||
const setup = (override = {}) => {
|
||||
const props = {
|
||||
item: '',
|
||||
buttonSize: '',
|
||||
isDisabled: false,
|
||||
confirmLeft: false,
|
||||
confirmTitle: '',
|
||||
onConfirm: () => {},
|
||||
onCancel: () => {},
|
||||
onClickOutside: () => {},
|
||||
...override,
|
||||
}
|
||||
|
||||
const wrapper = shallow(<ConfirmButtons {...props} />)
|
||||
|
||||
return {
|
||||
props,
|
||||
wrapper,
|
||||
}
|
||||
}
|
||||
|
||||
describe('Componenets.Shared.ConfirmButtons', () => {
|
||||
describe('rendering', () => {
|
||||
it('has a confirm and cancel button', () => {
|
||||
const {wrapper} = setup()
|
||||
const confirm = wrapper.dive().find(Confirm)
|
||||
const cancel = wrapper.dive().find(Cancel)
|
||||
|
||||
expect(confirm.exists()).toBe(true)
|
||||
expect(cancel.exists()).toBe(true)
|
||||
})
|
||||
|
||||
describe('confirmLeft is true', () => {
|
||||
it('has a confirm button to the left of the cancel button', () => {
|
||||
const {wrapper} = setup({confirmLeft: true})
|
||||
const buttons = wrapper.dive().children()
|
||||
const firstButton = buttons.first()
|
||||
const lastButton = buttons.last()
|
||||
|
||||
expect(firstButton.find(Confirm).exists()).toBe(true)
|
||||
expect(lastButton.find(Cancel).exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('confirmLeft is false', () => {
|
||||
it('has a confirm button to the right of the cancel button', () => {
|
||||
const {wrapper} = setup({confirmLeft: false})
|
||||
const buttons = wrapper.dive().children()
|
||||
const firstButton = buttons.first()
|
||||
const lastButton = buttons.last()
|
||||
|
||||
expect(firstButton.find(Cancel).exists()).toBe(true)
|
||||
expect(lastButton.find(Confirm).exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('confirmTitle', () => {
|
||||
describe('is not defined', () => {
|
||||
it('has a default title', () => {
|
||||
const {wrapper} = setup({confirmTitle: undefined})
|
||||
const confirm = wrapper.dive().find(Confirm)
|
||||
const title = confirm.prop('title')
|
||||
|
||||
expect(title.length).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('is defined', () => {
|
||||
it('has the title passed in as props', () => {
|
||||
const confirmTitle = 'delete'
|
||||
const {wrapper} = setup({confirmTitle})
|
||||
const confirm = wrapper.dive().find(Confirm)
|
||||
const title = confirm.prop('title')
|
||||
|
||||
expect(title).toContain(confirmTitle)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('user interaction', () => {
|
||||
describe('when clicking confirm', () => {
|
||||
it('should fire onConfirm', () => {
|
||||
const onConfirm = jest.fn()
|
||||
const item = 'test-item'
|
||||
const {wrapper} = setup({onConfirm, item})
|
||||
const confirm = wrapper.dive().find(Confirm)
|
||||
const button = confirm.dive().find({'data-test': 'confirm'})
|
||||
button.simulate('click')
|
||||
|
||||
expect(onConfirm).toHaveBeenCalledTimes(1)
|
||||
expect(onConfirm).toHaveBeenCalledWith(item)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when clicking cancel', () => {
|
||||
it('should fire onCancel', () => {
|
||||
const onCancel = jest.fn()
|
||||
const item = 'test-item'
|
||||
const {wrapper} = setup({onCancel, item})
|
||||
const cancel = wrapper.dive().find(Cancel)
|
||||
const button = cancel.dive().find({'data-test': 'cancel'})
|
||||
button.simulate('click')
|
||||
|
||||
expect(onCancel).toHaveBeenCalledTimes(1)
|
||||
expect(onCancel).toHaveBeenCalledWith(item)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,4 +1,4 @@
|
|||
Copyright (c) 2009 The oauth2 Authors. All rights reserved.
|
||||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
|
|
|
@ -11,6 +11,9 @@ oauth2 package contains a client implementation for OAuth 2.0 spec.
|
|||
go get golang.org/x/oauth2
|
||||
~~~~
|
||||
|
||||
Or you can manually git clone the repository to
|
||||
`$(go env GOPATH)/src/golang.org/x/oauth2`.
|
||||
|
||||
See godoc for further documentation and examples.
|
||||
|
||||
* [godoc.org/golang.org/x/oauth2](http://godoc.org/golang.org/x/oauth2)
|
||||
|
@ -19,11 +22,11 @@ See godoc for further documentation and examples.
|
|||
|
||||
## App Engine
|
||||
|
||||
In change 96e89be (March 2015) we removed the `oauth2.Context2` type in favor
|
||||
In change 96e89be (March 2015), we removed the `oauth2.Context2` type in favor
|
||||
of the [`context.Context`](https://golang.org/x/net/context#Context) type from
|
||||
the `golang.org/x/net/context` package
|
||||
|
||||
This means its no longer possible to use the "Classic App Engine"
|
||||
This means it's no longer possible to use the "Classic App Engine"
|
||||
`appengine.Context` type with the `oauth2` package. (You're using
|
||||
Classic App Engine if you import the package `"appengine"`.)
|
||||
|
||||
|
@ -39,27 +42,36 @@ If you don't want to update your entire app to use the new App Engine packages,
|
|||
you may use both sets of packages in parallel, using only the new packages
|
||||
with the `oauth2` package.
|
||||
|
||||
import (
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google"
|
||||
newappengine "google.golang.org/appengine"
|
||||
newurlfetch "google.golang.org/appengine/urlfetch"
|
||||
```go
|
||||
import (
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google"
|
||||
newappengine "google.golang.org/appengine"
|
||||
newurlfetch "google.golang.org/appengine/urlfetch"
|
||||
|
||||
"appengine"
|
||||
)
|
||||
"appengine"
|
||||
)
|
||||
|
||||
func handler(w http.ResponseWriter, r *http.Request) {
|
||||
var c appengine.Context = appengine.NewContext(r)
|
||||
c.Infof("Logging a message with the old package")
|
||||
func handler(w http.ResponseWriter, r *http.Request) {
|
||||
var c appengine.Context = appengine.NewContext(r)
|
||||
c.Infof("Logging a message with the old package")
|
||||
|
||||
var ctx context.Context = newappengine.NewContext(r)
|
||||
client := &http.Client{
|
||||
Transport: &oauth2.Transport{
|
||||
Source: google.AppEngineTokenSource(ctx, "scope"),
|
||||
Base: &newurlfetch.Transport{Context: ctx},
|
||||
},
|
||||
}
|
||||
client.Get("...")
|
||||
var ctx context.Context = newappengine.NewContext(r)
|
||||
client := &http.Client{
|
||||
Transport: &oauth2.Transport{
|
||||
Source: google.AppEngineTokenSource(ctx, "scope"),
|
||||
Base: &newurlfetch.Transport{Context: ctx},
|
||||
},
|
||||
}
|
||||
client.Get("...")
|
||||
}
|
||||
```
|
||||
|
||||
## Report Issues / Send Patches
|
||||
|
||||
This repository uses Gerrit for code changes. To learn how to submit changes to
|
||||
this repository, see https://golang.org/doc/contribute.html.
|
||||
|
||||
The main issue tracker for the oauth2 repository is located at
|
||||
https://github.com/golang/oauth2/issues.
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
// 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.
|
||||
|
||||
// +build appengine
|
||||
|
||||
// App Engine hooks.
|
||||
|
||||
package oauth2
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/oauth2/internal"
|
||||
"google.golang.org/appengine/urlfetch"
|
||||
)
|
||||
|
||||
func init() {
|
||||
internal.RegisterContextClientFunc(contextClientAppEngine)
|
||||
}
|
||||
|
||||
func contextClientAppEngine(ctx context.Context) (*http.Client, error) {
|
||||
return urlfetch.Client(ctx), nil
|
||||
}
|
|
@ -8,6 +8,8 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
@ -45,3 +47,43 @@ func ExampleConfig() {
|
|||
client := conf.Client(ctx, tok)
|
||||
client.Get("...")
|
||||
}
|
||||
|
||||
func ExampleConfig_customHTTP() {
|
||||
ctx := context.Background()
|
||||
|
||||
conf := &oauth2.Config{
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
Scopes: []string{"SCOPE1", "SCOPE2"},
|
||||
Endpoint: oauth2.Endpoint{
|
||||
TokenURL: "https://provider.com/o/oauth2/token",
|
||||
AuthURL: "https://provider.com/o/oauth2/auth",
|
||||
},
|
||||
}
|
||||
|
||||
// Redirect user to consent page to ask for permission
|
||||
// for the scopes specified above.
|
||||
url := conf.AuthCodeURL("state", oauth2.AccessTypeOffline)
|
||||
fmt.Printf("Visit the URL for the auth dialog: %v", url)
|
||||
|
||||
// Use the authorization code that is pushed to the redirect
|
||||
// URL. Exchange will do the handshake to retrieve the
|
||||
// initial access token. The HTTP Client returned by
|
||||
// conf.Client will refresh the token as necessary.
|
||||
var code string
|
||||
if _, err := fmt.Scan(&code); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Use the custom HTTP client when requesting a token.
|
||||
httpClient := &http.Client{Timeout: 2 * time.Second}
|
||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
|
||||
|
||||
tok, err := conf.Exchange(ctx, code)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
client := conf.Client(ctx, tok)
|
||||
_ = client
|
||||
}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
// Copyright 2018 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.
|
||||
|
||||
// +build appengine
|
||||
|
||||
package internal
|
||||
|
||||
import "google.golang.org/appengine/urlfetch"
|
||||
|
||||
func init() {
|
||||
appengineClientHook = urlfetch.Client
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
// Copyright 2017 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 internal contains support packages for oauth2 package.
|
||||
package internal
|
|
@ -2,18 +2,14 @@
|
|||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package internal contains support packages for oauth2 package.
|
||||
package internal
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ParseKey converts the binary contents of a private key file
|
||||
|
@ -39,38 +35,3 @@ func ParseKey(key []byte) (*rsa.PrivateKey, error) {
|
|||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func ParseINI(ini io.Reader) (map[string]map[string]string, error) {
|
||||
result := map[string]map[string]string{
|
||||
"": map[string]string{}, // root section
|
||||
}
|
||||
scanner := bufio.NewScanner(ini)
|
||||
currentSection := ""
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if strings.HasPrefix(line, ";") {
|
||||
// comment.
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
||||
currentSection = strings.TrimSpace(line[1 : len(line)-1])
|
||||
result[currentSection] = map[string]string{}
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) == 2 && parts[0] != "" {
|
||||
result[currentSection][strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, fmt.Errorf("error scanning ini: %v", err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func CondVal(v string) []string {
|
||||
if v == "" {
|
||||
return nil
|
||||
}
|
||||
return []string{v}
|
||||
}
|
||||
|
|
|
@ -1,62 +0,0 @@
|
|||
// 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 internal contains support packages for oauth2 package.
|
||||
package internal
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseINI(t *testing.T) {
|
||||
tests := []struct {
|
||||
ini string
|
||||
want map[string]map[string]string
|
||||
}{
|
||||
{
|
||||
`root = toor
|
||||
[foo]
|
||||
bar = hop
|
||||
ini = nin
|
||||
`,
|
||||
map[string]map[string]string{
|
||||
"": map[string]string{"root": "toor"},
|
||||
"foo": map[string]string{"bar": "hop", "ini": "nin"},
|
||||
},
|
||||
},
|
||||
{
|
||||
`[empty]
|
||||
[section]
|
||||
empty=
|
||||
`,
|
||||
map[string]map[string]string{
|
||||
"": map[string]string{},
|
||||
"empty": map[string]string{},
|
||||
"section": map[string]string{"empty": ""},
|
||||
},
|
||||
},
|
||||
{
|
||||
`ignore
|
||||
[invalid
|
||||
=stuff
|
||||
;comment=true
|
||||
`,
|
||||
map[string]map[string]string{
|
||||
"": map[string]string{},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
result, err := ParseINI(strings.NewReader(tt.ini))
|
||||
if err != nil {
|
||||
t.Errorf("ParseINI(%q) error %v, want: no error", tt.ini, err)
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(result, tt.want) {
|
||||
t.Errorf("ParseINI(%q) = %#v, want: %#v", tt.ini, result, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,11 +2,11 @@
|
|||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package internal contains support packages for oauth2 package.
|
||||
package internal
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
@ -18,9 +18,10 @@ import (
|
|||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/net/context/ctxhttp"
|
||||
)
|
||||
|
||||
// Token represents the crendentials used to authorize
|
||||
// Token represents the credentials used to authorize
|
||||
// the requests to access protected resources on the OAuth 2.0
|
||||
// provider's backend.
|
||||
//
|
||||
|
@ -91,6 +92,7 @@ func (e *expirationTime) UnmarshalJSON(b []byte) error {
|
|||
|
||||
var brokenAuthHeaderProviders = []string{
|
||||
"https://accounts.google.com/",
|
||||
"https://api.codeswholesale.com/oauth/token",
|
||||
"https://api.dropbox.com/",
|
||||
"https://api.dropboxapi.com/",
|
||||
"https://api.instagram.com/",
|
||||
|
@ -101,8 +103,11 @@ var brokenAuthHeaderProviders = []string{
|
|||
"https://api.twitch.tv/",
|
||||
"https://app.box.com/",
|
||||
"https://connect.stripe.com/",
|
||||
"https://login.mailchimp.com/",
|
||||
"https://login.microsoftonline.com/",
|
||||
"https://login.salesforce.com/",
|
||||
"https://login.windows.net",
|
||||
"https://login.live.com/",
|
||||
"https://oauth.sandbox.trainingpeaks.com/",
|
||||
"https://oauth.trainingpeaks.com/",
|
||||
"https://oauth.vk.com/",
|
||||
|
@ -117,6 +122,19 @@ var brokenAuthHeaderProviders = []string{
|
|||
"https://www.strava.com/oauth/",
|
||||
"https://www.wunderlist.com/oauth/",
|
||||
"https://api.patreon.com/",
|
||||
"https://sandbox.codeswholesale.com/oauth/token",
|
||||
"https://api.sipgate.com/v1/authorization/oauth",
|
||||
"https://api.medium.com/v1/tokens",
|
||||
"https://log.finalsurge.com/oauth/token",
|
||||
}
|
||||
|
||||
// brokenAuthHeaderDomains lists broken providers that issue dynamic endpoints.
|
||||
var brokenAuthHeaderDomains = []string{
|
||||
".auth0.com",
|
||||
".force.com",
|
||||
".myshopify.com",
|
||||
".okta.com",
|
||||
".oktapreview.com",
|
||||
}
|
||||
|
||||
func RegisterBrokenAuthHeaderProvider(tokenURL string) {
|
||||
|
@ -139,6 +157,14 @@ func providerAuthHeaderWorks(tokenURL string) bool {
|
|||
}
|
||||
}
|
||||
|
||||
if u, err := url.Parse(tokenURL); err == nil {
|
||||
for _, s := range brokenAuthHeaderDomains {
|
||||
if strings.HasSuffix(u.Host, s) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Assume the provider implements the spec properly
|
||||
// otherwise. We can add more exceptions as they're
|
||||
// discovered. We will _not_ be adding configurable hooks
|
||||
|
@ -147,14 +173,14 @@ func providerAuthHeaderWorks(tokenURL string) bool {
|
|||
}
|
||||
|
||||
func RetrieveToken(ctx context.Context, clientID, clientSecret, tokenURL string, v url.Values) (*Token, error) {
|
||||
hc, err := ContextClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v.Set("client_id", clientID)
|
||||
bustedAuth := !providerAuthHeaderWorks(tokenURL)
|
||||
if bustedAuth && clientSecret != "" {
|
||||
v.Set("client_secret", clientSecret)
|
||||
if bustedAuth {
|
||||
if clientID != "" {
|
||||
v.Set("client_id", clientID)
|
||||
}
|
||||
if clientSecret != "" {
|
||||
v.Set("client_secret", clientSecret)
|
||||
}
|
||||
}
|
||||
req, err := http.NewRequest("POST", tokenURL, strings.NewReader(v.Encode()))
|
||||
if err != nil {
|
||||
|
@ -162,9 +188,9 @@ func RetrieveToken(ctx context.Context, clientID, clientSecret, tokenURL string,
|
|||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
if !bustedAuth {
|
||||
req.SetBasicAuth(clientID, clientSecret)
|
||||
req.SetBasicAuth(url.QueryEscape(clientID), url.QueryEscape(clientSecret))
|
||||
}
|
||||
r, err := hc.Do(req)
|
||||
r, err := ctxhttp.Do(ctx, ContextClient(ctx), req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -174,7 +200,10 @@ func RetrieveToken(ctx context.Context, clientID, clientSecret, tokenURL string,
|
|||
return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
|
||||
}
|
||||
if code := r.StatusCode; code < 200 || code > 299 {
|
||||
return nil, fmt.Errorf("oauth2: cannot fetch token: %v\nResponse: %s", r.Status, body)
|
||||
return nil, &RetrieveError{
|
||||
Response: r,
|
||||
Body: body,
|
||||
}
|
||||
}
|
||||
|
||||
var token *Token
|
||||
|
@ -221,5 +250,17 @@ func RetrieveToken(ctx context.Context, clientID, clientSecret, tokenURL string,
|
|||
if token.RefreshToken == "" {
|
||||
token.RefreshToken = v.Get("refresh_token")
|
||||
}
|
||||
if token.AccessToken == "" {
|
||||
return token, errors.New("oauth2: server response missing access_token")
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
type RetrieveError struct {
|
||||
Response *http.Response
|
||||
Body []byte
|
||||
}
|
||||
|
||||
func (r *RetrieveError) Error() string {
|
||||
return fmt.Sprintf("oauth2: cannot fetch token: %v\nResponse: %s", r.Response.Status, r.Body)
|
||||
}
|
||||
|
|
|
@ -2,12 +2,17 @@
|
|||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package internal contains support packages for oauth2 package.
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func TestRegisterBrokenAuthHeaderProvider(t *testing.T) {
|
||||
|
@ -18,6 +23,28 @@ func TestRegisterBrokenAuthHeaderProvider(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestRetrieveTokenBustedNoSecret(t *testing.T) {
|
||||
const clientID = "client-id"
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if got, want := r.FormValue("client_id"), clientID; got != want {
|
||||
t.Errorf("client_id = %q; want %q", got, want)
|
||||
}
|
||||
if got, want := r.FormValue("client_secret"), ""; got != want {
|
||||
t.Errorf("client_secret = %q; want empty", got)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
io.WriteString(w, `{"access_token": "ACCESS_TOKEN", "token_type": "bearer"}`)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
RegisterBrokenAuthHeaderProvider(ts.URL)
|
||||
_, err := RetrieveToken(context.Background(), clientID, "", ts.URL, url.Values{})
|
||||
if err != nil {
|
||||
t.Errorf("RetrieveToken = %v; want no error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_providerAuthHeaderWorks(t *testing.T) {
|
||||
for _, p := range brokenAuthHeaderProviders {
|
||||
if providerAuthHeaderWorks(p) {
|
||||
|
@ -33,3 +60,53 @@ func Test_providerAuthHeaderWorks(t *testing.T) {
|
|||
t.Errorf("got %q as unbroken; want broken", p)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderAuthHeaderWorksDomain(t *testing.T) {
|
||||
tests := []struct {
|
||||
tokenURL string
|
||||
wantWorks bool
|
||||
}{
|
||||
{"https://dev-12345.okta.com/token-url", false},
|
||||
{"https://dev-12345.oktapreview.com/token-url", false},
|
||||
{"https://dev-12345.okta.org/token-url", true},
|
||||
{"https://foo.bar.force.com/token-url", false},
|
||||
{"https://foo.force.com/token-url", false},
|
||||
{"https://force.com/token-url", true},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
got := providerAuthHeaderWorks(test.tokenURL)
|
||||
if got != test.wantWorks {
|
||||
t.Errorf("providerAuthHeaderWorks(%q) = %v; want %v", test.tokenURL, got, test.wantWorks)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveTokenWithContexts(t *testing.T) {
|
||||
const clientID = "client-id"
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
io.WriteString(w, `{"access_token": "ACCESS_TOKEN", "token_type": "bearer"}`)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
_, err := RetrieveToken(context.Background(), clientID, "", ts.URL, url.Values{})
|
||||
if err != nil {
|
||||
t.Errorf("RetrieveToken (with background context) = %v; want no error", err)
|
||||
}
|
||||
|
||||
retrieved := make(chan struct{})
|
||||
cancellingts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
<-retrieved
|
||||
}))
|
||||
defer cancellingts.Close()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
_, err = RetrieveToken(ctx, clientID, "", cancellingts.URL, url.Values{})
|
||||
close(retrieved)
|
||||
if err == nil {
|
||||
t.Errorf("RetrieveToken (with cancelled context) = nil; want error")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package internal contains support packages for oauth2 package.
|
||||
package internal
|
||||
|
||||
import (
|
||||
|
@ -20,50 +19,16 @@ var HTTPClient ContextKey
|
|||
// because nobody else can create a ContextKey, being unexported.
|
||||
type ContextKey struct{}
|
||||
|
||||
// ContextClientFunc is a func which tries to return an *http.Client
|
||||
// given a Context value. If it returns an error, the search stops
|
||||
// with that error. If it returns (nil, nil), the search continues
|
||||
// down the list of registered funcs.
|
||||
type ContextClientFunc func(context.Context) (*http.Client, error)
|
||||
var appengineClientHook func(context.Context) *http.Client
|
||||
|
||||
var contextClientFuncs []ContextClientFunc
|
||||
|
||||
func RegisterContextClientFunc(fn ContextClientFunc) {
|
||||
contextClientFuncs = append(contextClientFuncs, fn)
|
||||
}
|
||||
|
||||
func ContextClient(ctx context.Context) (*http.Client, error) {
|
||||
func ContextClient(ctx context.Context) *http.Client {
|
||||
if ctx != nil {
|
||||
if hc, ok := ctx.Value(HTTPClient).(*http.Client); ok {
|
||||
return hc, nil
|
||||
return hc
|
||||
}
|
||||
}
|
||||
for _, fn := range contextClientFuncs {
|
||||
c, err := fn(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if c != nil {
|
||||
return c, nil
|
||||
}
|
||||
if appengineClientHook != nil {
|
||||
return appengineClientHook(ctx)
|
||||
}
|
||||
return http.DefaultClient, nil
|
||||
}
|
||||
|
||||
func ContextTransport(ctx context.Context) http.RoundTripper {
|
||||
hc, err := ContextClient(ctx)
|
||||
// This is a rare error case (somebody using nil on App Engine).
|
||||
if err != nil {
|
||||
return ErrorTransport{err}
|
||||
}
|
||||
return hc.Transport
|
||||
}
|
||||
|
||||
// ErrorTransport returns the specified error on RoundTrip.
|
||||
// This RoundTripper should be used in rare error cases where
|
||||
// error handling can be postponed to response handling time.
|
||||
type ErrorTransport struct{ Err error }
|
||||
|
||||
func (t ErrorTransport) RoundTrip(*http.Request) (*http.Response, error) {
|
||||
return nil, t.Err
|
||||
return http.DefaultClient
|
||||
}
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
// Copyright 2015 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 internal
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func TestContextClient(t *testing.T) {
|
||||
rc := &http.Client{}
|
||||
RegisterContextClientFunc(func(context.Context) (*http.Client, error) {
|
||||
return rc, nil
|
||||
})
|
||||
|
||||
c := &http.Client{}
|
||||
ctx := context.WithValue(context.Background(), HTTPClient, c)
|
||||
|
||||
hc, err := ContextClient(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("want valid client; got err = %v", err)
|
||||
}
|
||||
if hc != c {
|
||||
t.Fatalf("want context client = %p; got = %p", c, hc)
|
||||
}
|
||||
|
||||
hc, err = ContextClient(context.TODO())
|
||||
if err != nil {
|
||||
t.Fatalf("want valid client; got err = %v", err)
|
||||
}
|
||||
if hc != rc {
|
||||
t.Fatalf("want registered client = %p; got = %p", c, hc)
|
||||
}
|
||||
}
|
|
@ -117,7 +117,7 @@ func SetAuthURLParam(key, value string) AuthCodeOption {
|
|||
// that asks for permissions for the required scopes explicitly.
|
||||
//
|
||||
// State is a token to protect the user from CSRF attacks. You must
|
||||
// always provide a non-zero string and validate that it matches the
|
||||
// always provide a non-empty string and validate that it matches the
|
||||
// the state query parameter on your redirect callback.
|
||||
// See http://tools.ietf.org/html/rfc6749#section-10.12 for more info.
|
||||
//
|
||||
|
@ -129,9 +129,16 @@ func (c *Config) AuthCodeURL(state string, opts ...AuthCodeOption) string {
|
|||
v := url.Values{
|
||||
"response_type": {"code"},
|
||||
"client_id": {c.ClientID},
|
||||
"redirect_uri": internal.CondVal(c.RedirectURL),
|
||||
"scope": internal.CondVal(strings.Join(c.Scopes, " ")),
|
||||
"state": internal.CondVal(state),
|
||||
}
|
||||
if c.RedirectURL != "" {
|
||||
v.Set("redirect_uri", c.RedirectURL)
|
||||
}
|
||||
if len(c.Scopes) > 0 {
|
||||
v.Set("scope", strings.Join(c.Scopes, " "))
|
||||
}
|
||||
if state != "" {
|
||||
// TODO(light): Docs say never to omit state; don't allow empty.
|
||||
v.Set("state", state)
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt.setValue(v)
|
||||
|
@ -157,12 +164,15 @@ func (c *Config) AuthCodeURL(state string, opts ...AuthCodeOption) string {
|
|||
// The HTTP client to use is derived from the context.
|
||||
// If nil, http.DefaultClient is used.
|
||||
func (c *Config) PasswordCredentialsToken(ctx context.Context, username, password string) (*Token, error) {
|
||||
return retrieveToken(ctx, c, url.Values{
|
||||
v := url.Values{
|
||||
"grant_type": {"password"},
|
||||
"username": {username},
|
||||
"password": {password},
|
||||
"scope": internal.CondVal(strings.Join(c.Scopes, " ")),
|
||||
})
|
||||
}
|
||||
if len(c.Scopes) > 0 {
|
||||
v.Set("scope", strings.Join(c.Scopes, " "))
|
||||
}
|
||||
return retrieveToken(ctx, c, v)
|
||||
}
|
||||
|
||||
// Exchange converts an authorization code into a token.
|
||||
|
@ -176,12 +186,14 @@ func (c *Config) PasswordCredentialsToken(ctx context.Context, username, passwor
|
|||
// The code will be in the *http.Request.FormValue("code"). Before
|
||||
// calling Exchange, be sure to validate FormValue("state").
|
||||
func (c *Config) Exchange(ctx context.Context, code string) (*Token, error) {
|
||||
return retrieveToken(ctx, c, url.Values{
|
||||
"grant_type": {"authorization_code"},
|
||||
"code": {code},
|
||||
"redirect_uri": internal.CondVal(c.RedirectURL),
|
||||
"scope": internal.CondVal(strings.Join(c.Scopes, " ")),
|
||||
})
|
||||
v := url.Values{
|
||||
"grant_type": {"authorization_code"},
|
||||
"code": {code},
|
||||
}
|
||||
if c.RedirectURL != "" {
|
||||
v.Set("redirect_uri", c.RedirectURL)
|
||||
}
|
||||
return retrieveToken(ctx, c, v)
|
||||
}
|
||||
|
||||
// Client returns an HTTP client using the provided token.
|
||||
|
@ -292,20 +304,20 @@ var HTTPClient internal.ContextKey
|
|||
// NewClient creates an *http.Client from a Context and TokenSource.
|
||||
// The returned client is not valid beyond the lifetime of the context.
|
||||
//
|
||||
// Note that if a custom *http.Client is provided via the Context it
|
||||
// is used only for token acquisition and is not used to configure the
|
||||
// *http.Client returned from NewClient.
|
||||
//
|
||||
// As a special case, if src is nil, a non-OAuth2 client is returned
|
||||
// using the provided context. This exists to support related OAuth2
|
||||
// packages.
|
||||
func NewClient(ctx context.Context, src TokenSource) *http.Client {
|
||||
if src == nil {
|
||||
c, err := internal.ContextClient(ctx)
|
||||
if err != nil {
|
||||
return &http.Client{Transport: internal.ErrorTransport{Err: err}}
|
||||
}
|
||||
return c
|
||||
return internal.ContextClient(ctx)
|
||||
}
|
||||
return &http.Client{
|
||||
Transport: &Transport{
|
||||
Base: internal.ContextTransport(ctx),
|
||||
Base: internal.ContextClient(ctx).Transport,
|
||||
Source: ReuseTokenSource(nil, src),
|
||||
},
|
||||
}
|
||||
|
|
|
@ -72,6 +72,25 @@ func TestAuthCodeURL_Optional(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestURLUnsafeClientConfig(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if got, want := r.Header.Get("Authorization"), "Basic Q0xJRU5UX0lEJTNGJTNGOkNMSUVOVF9TRUNSRVQlM0YlM0Y="; got != want {
|
||||
t.Errorf("Authorization header = %q; want %q", got, want)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
w.Write([]byte("access_token=90d64460d14870c08c81352a05dedd3465940a7c&scope=user&token_type=bearer"))
|
||||
}))
|
||||
defer ts.Close()
|
||||
conf := newConf(ts.URL)
|
||||
conf.ClientID = "CLIENT_ID??"
|
||||
conf.ClientSecret = "CLIENT_SECRET??"
|
||||
_, err := conf.Exchange(context.Background(), "exchange-code")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExchangeRequest(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.String() != "/token" {
|
||||
|
@ -89,7 +108,7 @@ func TestExchangeRequest(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Errorf("Failed reading request body: %s.", err)
|
||||
}
|
||||
if string(body) != "client_id=CLIENT_ID&code=exchange-code&grant_type=authorization_code&redirect_uri=REDIRECT_URL&scope=scope1+scope2" {
|
||||
if string(body) != "code=exchange-code&grant_type=authorization_code&redirect_uri=REDIRECT_URL" {
|
||||
t.Errorf("Unexpected exchange payload, %v is found.", string(body))
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
@ -133,7 +152,7 @@ func TestExchangeRequest_JSONResponse(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Errorf("Failed reading request body: %s.", err)
|
||||
}
|
||||
if string(body) != "client_id=CLIENT_ID&code=exchange-code&grant_type=authorization_code&redirect_uri=REDIRECT_URL&scope=scope1+scope2" {
|
||||
if string(body) != "code=exchange-code&grant_type=authorization_code&redirect_uri=REDIRECT_URL" {
|
||||
t.Errorf("Unexpected exchange payload, %v is found.", string(body))
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
@ -259,12 +278,9 @@ func TestExchangeRequest_BadResponse(t *testing.T) {
|
|||
}))
|
||||
defer ts.Close()
|
||||
conf := newConf(ts.URL)
|
||||
tok, err := conf.Exchange(context.Background(), "code")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if tok.AccessToken != "" {
|
||||
t.Errorf("Unexpected access token, %#v.", tok.AccessToken)
|
||||
_, err := conf.Exchange(context.Background(), "code")
|
||||
if err == nil {
|
||||
t.Error("expected error from missing access_token")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -277,7 +293,7 @@ func TestExchangeRequest_BadResponseType(t *testing.T) {
|
|||
conf := newConf(ts.URL)
|
||||
_, err := conf.Exchange(context.Background(), "exchange-code")
|
||||
if err == nil {
|
||||
t.Error("expected error from invalid access_token type")
|
||||
t.Error("expected error from non-string access_token")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -325,7 +341,7 @@ func TestPasswordCredentialsTokenRequest(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Errorf("Failed reading request body: %s.", err)
|
||||
}
|
||||
expected = "client_id=CLIENT_ID&grant_type=password&password=password1&scope=scope1+scope2&username=user1"
|
||||
expected = "grant_type=password&password=password1&scope=scope1+scope2&username=user1"
|
||||
if string(body) != expected {
|
||||
t.Errorf("res.Body = %q; want %q", string(body), expected)
|
||||
}
|
||||
|
@ -364,7 +380,7 @@ func TestTokenRefreshRequest(t *testing.T) {
|
|||
t.Errorf("Unexpected Content-Type header, %v is found.", headerContentType)
|
||||
}
|
||||
body, _ := ioutil.ReadAll(r.Body)
|
||||
if string(body) != "client_id=CLIENT_ID&grant_type=refresh_token&refresh_token=REFRESH_TOKEN" {
|
||||
if string(body) != "grant_type=refresh_token&refresh_token=REFRESH_TOKEN" {
|
||||
t.Errorf("Unexpected refresh token payload, %v is found.", string(body))
|
||||
}
|
||||
}))
|
||||
|
@ -400,26 +416,67 @@ func TestFetchWithNoRefreshToken(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestTokenRetrieveError(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.String() != "/token" {
|
||||
t.Errorf("Unexpected token refresh request URL, %v is found.", r.URL)
|
||||
}
|
||||
w.Header().Set("Content-type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte(`{"error": "invalid_grant"}`))
|
||||
}))
|
||||
defer ts.Close()
|
||||
conf := newConf(ts.URL)
|
||||
_, err := conf.Exchange(context.Background(), "exchange-code")
|
||||
if err == nil {
|
||||
t.Fatalf("got no error, expected one")
|
||||
}
|
||||
_, ok := err.(*RetrieveError)
|
||||
if !ok {
|
||||
t.Fatalf("got %T error, expected *RetrieveError", err)
|
||||
}
|
||||
// Test error string for backwards compatibility
|
||||
expected := fmt.Sprintf("oauth2: cannot fetch token: %v\nResponse: %s", "400 Bad Request", `{"error": "invalid_grant"}`)
|
||||
if errStr := err.Error(); errStr != expected {
|
||||
t.Fatalf("got %#v, expected %#v", errStr, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefreshToken_RefreshTokenReplacement(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"access_token":"ACCESS TOKEN", "scope": "user", "token_type": "bearer", "refresh_token": "NEW REFRESH TOKEN"}`))
|
||||
w.Write([]byte(`{"access_token":"ACCESS_TOKEN", "scope": "user", "token_type": "bearer", "refresh_token": "NEW_REFRESH_TOKEN"}`))
|
||||
return
|
||||
}))
|
||||
defer ts.Close()
|
||||
conf := newConf(ts.URL)
|
||||
tkr := tokenRefresher{
|
||||
conf: conf,
|
||||
ctx: context.Background(),
|
||||
refreshToken: "OLD REFRESH TOKEN",
|
||||
}
|
||||
tkr := conf.TokenSource(context.Background(), &Token{RefreshToken: "OLD_REFRESH_TOKEN"})
|
||||
tk, err := tkr.Token()
|
||||
if err != nil {
|
||||
t.Errorf("got err = %v; want none", err)
|
||||
return
|
||||
}
|
||||
if tk.RefreshToken != tkr.refreshToken {
|
||||
t.Errorf("tokenRefresher.refresh_token = %q; want %q", tkr.refreshToken, tk.RefreshToken)
|
||||
if want := "NEW_REFRESH_TOKEN"; tk.RefreshToken != want {
|
||||
t.Errorf("RefreshToken = %q; want %q", tk.RefreshToken, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefreshToken_RefreshTokenPreservation(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"access_token":"ACCESS_TOKEN", "scope": "user", "token_type": "bearer"}`))
|
||||
return
|
||||
}))
|
||||
defer ts.Close()
|
||||
conf := newConf(ts.URL)
|
||||
const oldRefreshToken = "OLD_REFRESH_TOKEN"
|
||||
tkr := conf.TokenSource(context.Background(), &Token{RefreshToken: oldRefreshToken})
|
||||
tk, err := tkr.Token()
|
||||
if err != nil {
|
||||
t.Fatalf("got err = %v; want none", err)
|
||||
}
|
||||
if tk.RefreshToken != oldRefreshToken {
|
||||
t.Errorf("RefreshToken = %q; want %q", tk.RefreshToken, oldRefreshToken)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
package oauth2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
@ -20,7 +21,7 @@ import (
|
|||
// expirations due to client-server time mismatches.
|
||||
const expiryDelta = 10 * time.Second
|
||||
|
||||
// Token represents the crendentials used to authorize
|
||||
// Token represents the credentials used to authorize
|
||||
// the requests to access protected resources on the OAuth 2.0
|
||||
// provider's backend.
|
||||
//
|
||||
|
@ -123,7 +124,7 @@ func (t *Token) expired() bool {
|
|||
if t.Expiry.IsZero() {
|
||||
return false
|
||||
}
|
||||
return t.Expiry.Add(-expiryDelta).Before(time.Now())
|
||||
return t.Expiry.Round(0).Add(-expiryDelta).Before(time.Now())
|
||||
}
|
||||
|
||||
// Valid reports whether t is non-nil, has an AccessToken, and is not expired.
|
||||
|
@ -152,7 +153,23 @@ func tokenFromInternal(t *internal.Token) *Token {
|
|||
func retrieveToken(ctx context.Context, c *Config, v url.Values) (*Token, error) {
|
||||
tk, err := internal.RetrieveToken(ctx, c.ClientID, c.ClientSecret, c.Endpoint.TokenURL, v)
|
||||
if err != nil {
|
||||
if rErr, ok := err.(*internal.RetrieveError); ok {
|
||||
return nil, (*RetrieveError)(rErr)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return tokenFromInternal(tk), nil
|
||||
}
|
||||
|
||||
// RetrieveError is the error returned when the token endpoint returns a
|
||||
// non-2XX HTTP status code.
|
||||
type RetrieveError struct {
|
||||
Response *http.Response
|
||||
// Body is the body that was consumed by reading Response.Body.
|
||||
// It may be truncated.
|
||||
Body []byte
|
||||
}
|
||||
|
||||
func (r *RetrieveError) Error() string {
|
||||
return fmt.Sprintf("oauth2: cannot fetch token: %v\nResponse: %s", r.Response.Status, r.Body)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue