Merge pull request #256 from influxdata/feature/github
Add authentication support via OAuth and JWTpull/272/head
commit
0bf9b75e45
4
Godeps
4
Godeps
|
@ -3,6 +3,7 @@ github.com/PuerkitoBio/urlesc 5bd2802263f21d8788851d5305584c82a5c75d7e
|
||||||
github.com/Sirupsen/logrus 3ec0642a7fb6488f65b06f9040adc67e3990296a
|
github.com/Sirupsen/logrus 3ec0642a7fb6488f65b06f9040adc67e3990296a
|
||||||
github.com/asaskevich/govalidator 593d64559f7600f29581a3ee42177f5dbded27a9
|
github.com/asaskevich/govalidator 593d64559f7600f29581a3ee42177f5dbded27a9
|
||||||
github.com/boltdb/bolt 5cc10bbbc5c141029940133bb33c9e969512a698
|
github.com/boltdb/bolt 5cc10bbbc5c141029940133bb33c9e969512a698
|
||||||
|
github.com/dgrijalva/jwt-go 24c63f56522a87ec5339cc3567883f1039378fdb
|
||||||
github.com/elazarl/go-bindata-assetfs 9a6736ed45b44bf3835afeebb3034b57ed329f3e
|
github.com/elazarl/go-bindata-assetfs 9a6736ed45b44bf3835afeebb3034b57ed329f3e
|
||||||
github.com/go-openapi/analysis b44dc874b601d9e4e2f6e19140e794ba24bead3b
|
github.com/go-openapi/analysis b44dc874b601d9e4e2f6e19140e794ba24bead3b
|
||||||
github.com/go-openapi/errors 4178436c9f2430cdd945c50301cfb61563b56573
|
github.com/go-openapi/errors 4178436c9f2430cdd945c50301cfb61563b56573
|
||||||
|
@ -15,10 +16,13 @@ github.com/go-openapi/strfmt d65c7fdb29eca313476e529628176fe17e58c488
|
||||||
github.com/go-openapi/swag 0e04f5e499b19bf51031c01a00f098f25067d8dc
|
github.com/go-openapi/swag 0e04f5e499b19bf51031c01a00f098f25067d8dc
|
||||||
github.com/go-openapi/validate deaf2c9013bc1a7f4c774662259a506ba874d80f
|
github.com/go-openapi/validate deaf2c9013bc1a7f4c774662259a506ba874d80f
|
||||||
github.com/gogo/protobuf 6abcf94fd4c97dcb423fdafd42fe9f96ca7e421b
|
github.com/gogo/protobuf 6abcf94fd4c97dcb423fdafd42fe9f96ca7e421b
|
||||||
|
github.com/google/go-github 1bc362c7737e51014af7299e016444b654095ad9
|
||||||
|
github.com/google/go-querystring 9235644dd9e52eeae6fa48efd539fdc351a0af53
|
||||||
github.com/gorilla/context 08b5f424b9271eedf6f9f0ce86cb9396ed337a42
|
github.com/gorilla/context 08b5f424b9271eedf6f9f0ce86cb9396ed337a42
|
||||||
github.com/jessevdk/go-flags 4cc2832a6e6d1d3b815e2b9d544b2a4dfb3ce8fa
|
github.com/jessevdk/go-flags 4cc2832a6e6d1d3b815e2b9d544b2a4dfb3ce8fa
|
||||||
github.com/mailru/easyjson e978125a7e335d8f4db746a9ac5b44643f27416b
|
github.com/mailru/easyjson e978125a7e335d8f4db746a9ac5b44643f27416b
|
||||||
github.com/satori/go.uuid b061729afc07e77a8aa4fad0a2fd840958f1942a
|
github.com/satori/go.uuid b061729afc07e77a8aa4fad0a2fd840958f1942a
|
||||||
github.com/tylerb/graceful 50a48b6e73fcc75b45e22c05b79629a67c79e938
|
github.com/tylerb/graceful 50a48b6e73fcc75b45e22c05b79629a67c79e938
|
||||||
golang.org/x/net 749a502dd1eaf3e5bfd4f8956748c502357c0bbe
|
golang.org/x/net 749a502dd1eaf3e5bfd4f8956748c502357c0bbe
|
||||||
|
golang.org/x/oauth2 1e695b1c8febf17aad3bfa7bf0a819ef94b98ad5
|
||||||
golang.org/x/text 1e65e9bf72c307081cea196f47ef37aed17eb316
|
golang.org/x/text 1e65e9bf72c307081cea196f47ef37aed17eb316
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
package chronograf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Principal is any entity that can be authenticated
|
||||||
|
type Principal string
|
||||||
|
|
||||||
|
// PrincipalKey is used to pass principal
|
||||||
|
// via context.Context to request-scoped
|
||||||
|
// functions.
|
||||||
|
const PrincipalKey Principal = "principal"
|
||||||
|
|
||||||
|
// Authenticator represents a service for authenticating users.
|
||||||
|
type Authenticator interface {
|
||||||
|
// Authenticate returns User associated with token if successful.
|
||||||
|
Authenticate(ctx context.Context, token string) (Principal, error)
|
||||||
|
// Token generates a valid token for Principal lasting a duration
|
||||||
|
Token(context.Context, Principal, time.Duration) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenExtractor extracts tokens from http requests
|
||||||
|
type TokenExtractor interface {
|
||||||
|
// Extract will return the token or an error.
|
||||||
|
Extract(r *http.Request) (string, error)
|
||||||
|
}
|
111
docs/auth.md
111
docs/auth.md
|
@ -1,9 +1,112 @@
|
||||||
Chronograf with OAuth 2.0 (Github-style)
|
## Chronograf with OAuth 2.0 (Github-style)
|
||||||
|
|
||||||
Originally Authored with Hackmd.io Link
|
|
||||||
|
|
||||||
OAuth 2.0 Style Authentication
|
OAuth 2.0 Style Authentication
|
||||||
|
|
||||||
Assumptions: The user has created an "OAuth Application" on Github to authenticate against.
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
To use authentication in Chronograf, both Github OAuth and JWT signature need to be configured.
|
||||||
|
|
||||||
|
#### Creating Github OAuth Application
|
||||||
|
|
||||||
|
To create a Github OAuth Application follow the [Register your app](https://developer.github.com/guides/basics-of-authentication/#registering-your-app) instructions.
|
||||||
|
Essentially, you'll register your application [here](https://github.com/settings/applications/new)
|
||||||
|
|
||||||
|
The `Homepage URL` should be Chronograf's full server name and port. If you are running it locally for example, make it `http://localhost:8888`
|
||||||
|
|
||||||
|
The `Authorization callback URL` must be the location of the `Homepage URL` plus `/oauth/github/callback`. For example, if `Homepage URL` was
|
||||||
|
`http://localhost:8888` then the `Authorization callback URL` should be `http://localhost:8888/oauth/github/callback`.
|
||||||
|
|
||||||
|
Github will provide a `Client ID` and `Client Secret`. To register these values with chronograf set the following environment variables:
|
||||||
|
|
||||||
|
* `GH_CLIENT_ID`
|
||||||
|
* `GH_CLIENT_SECRET`
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
export GH_CLIENT_ID=b339dd4fddd95abec9aa
|
||||||
|
export GH_CLIENT_SECRET=260041897d3252c146ece6b46ba39bc1e54416dc
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Configuring JWT signature
|
||||||
|
|
||||||
|
Set a [JWT](https://tools.ietf.org/html/rfc7519) signature to a random string.
|
||||||
|
*Keep this random string around!*
|
||||||
|
|
||||||
|
You'll need it each time you start a chronograf server because it is used to verify
|
||||||
|
user authorization.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
export TOKEN_SECRET=supersupersecret
|
||||||
|
```
|
||||||
|
|
||||||
|
### Design
|
||||||
|
|
||||||
|
The Chronograf authentication scheme is a standard [web application](https://developer.github.com/v3/oauth/#web-application-flow) OAuth flow.
|
||||||
|
|
||||||
![oauth 2.0 flow](./OauthStyleAuthentication.png)
|
![oauth 2.0 flow](./OauthStyleAuthentication.png)
|
||||||
|
|
||||||
|
The browser receives a cookie from Chronograf, authorizing it. The contents of the cookie is a JWT whose "sub" claim is the user's primary
|
||||||
|
github email address.
|
||||||
|
|
||||||
|
On each request to Chronograf, the JWT contained in the cookie will be validated against the `TOKEN_SECRET` signature and checked for expiration.
|
||||||
|
The JWT's "sub" becomes the [principal](https://en.wikipedia.org/wiki/Principal_(computer_security)) used for authorization to resources.
|
||||||
|
|
||||||
|
The API provides three endpoints `/oauth`, `/oauth/logout` and `/oauth/github/callback`.
|
||||||
|
|
||||||
|
#### /oauth
|
||||||
|
|
||||||
|
The `/oauth` endpoint redirects to Github for OAuth. Chronograf sets the OAuth `state` request parameter to a JWT with a random "sub". Using $TOKEN_SECRET `/oauth/github/callback`
|
||||||
|
can validate the `state` parameter without needing `state` to be saved.
|
||||||
|
|
||||||
|
#### /oauth/github/callback
|
||||||
|
|
||||||
|
The `/oauth/github/callback` receives the OAuth `authorization code` and `state`.
|
||||||
|
|
||||||
|
First, it will validate the `state` JWT from the `/oauth` endpoint. `JWT` validation
|
||||||
|
only requires access to the signature token. Therefore, there is no need for `state`
|
||||||
|
to be saved. Additionally, multiple Chronograf servers will not need to share third
|
||||||
|
party storage to synchronize `state`. If this validation fails, the request
|
||||||
|
will be redirected to `/login`.
|
||||||
|
|
||||||
|
Secondly, the endpoint will use the `authorization code` to retrieve a valid OAuth token
|
||||||
|
with the `user:email` scope. If unable to get a token from Github, the request will
|
||||||
|
be redirected to `/login`.
|
||||||
|
|
||||||
|
Finally, the endpoint will attempt to get the primary email address of the Github user.
|
||||||
|
Again, if not successful, the request will redirect to `/login`.
|
||||||
|
|
||||||
|
The email address is used as the subject claim for a new JWT. This JWT becomes the
|
||||||
|
value of the cookie sent back to the browser. The cookie is valid for thirty days.
|
||||||
|
|
||||||
|
Next, the request is redirected to `/`.
|
||||||
|
|
||||||
|
For all API calls to `/chronograf/v1`, the server checks for the existence and validity
|
||||||
|
of the JWT within the cookie value.
|
||||||
|
If the request did not have a valid JWT, the API returns `HTTP/1.1 401 Unauthorized`.
|
||||||
|
|
||||||
|
#### /oauth/logout
|
||||||
|
|
||||||
|
Simply expires the session cookie and redirects to `/`.
|
||||||
|
|
||||||
|
### Authorization
|
||||||
|
|
||||||
|
After successful validation of the JWT, each API endpoint of `/chronograf/v1` receives the
|
||||||
|
JWT subject within the `http.Request` as a `context.Context` value.
|
||||||
|
|
||||||
|
Within the Go API code all interfaces take `context.Context`. This means that each
|
||||||
|
interface can use the value as a principal. The design allows for authorization to happen
|
||||||
|
at the level of design most closely related to the problem.
|
||||||
|
|
||||||
|
An example usage in Go would be:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func ShallIPass(ctx context.Context) (string, error) {
|
||||||
|
principal := ctx.Value(mrfusion.PrincipalKey).(mrfusion.Principal)
|
||||||
|
if principal != "gandolf@moria.misty.mt" {
|
||||||
|
return "you shall not pass", mrfusion.ErrAuthentication
|
||||||
|
}
|
||||||
|
return "run you fools", nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
|
@ -7,6 +7,7 @@ const (
|
||||||
ErrSourceNotFound = Error("source not found")
|
ErrSourceNotFound = Error("source not found")
|
||||||
ErrServerNotFound = Error("server not found")
|
ErrServerNotFound = Error("server not found")
|
||||||
ErrLayoutNotFound = Error("layout not found")
|
ErrLayoutNotFound = Error("layout not found")
|
||||||
|
ErrAuthentication = Error("user not authenticated")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Error is a domain error encountered while processing chronograf requests
|
// Error is a domain error encountered while processing chronograf requests
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/influxdata/chronograf"
|
||||||
|
"github.com/influxdata/chronograf/dist"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Dir = "ui/build"
|
||||||
|
Default = "ui/build/index.html"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AssetsOpts configures the asset middleware
|
||||||
|
type AssetsOpts struct {
|
||||||
|
// Develop when true serves assets from ui/build directory directly; false will use internal bindata.
|
||||||
|
Develop bool
|
||||||
|
// Logger will log the asset served
|
||||||
|
Logger chronograf.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assets creates a middleware that will serve a single page app.
|
||||||
|
func Assets(opts AssetsOpts) http.Handler {
|
||||||
|
var assets chronograf.Assets
|
||||||
|
if opts.Develop {
|
||||||
|
assets = &dist.DebugAssets{
|
||||||
|
Dir: Dir,
|
||||||
|
Default: Default,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assets = &dist.BindataAssets{
|
||||||
|
Prefix: Dir,
|
||||||
|
Default: Default,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if opts.Logger != nil {
|
||||||
|
opts.Logger.
|
||||||
|
WithField("component", "server").
|
||||||
|
WithField("remote_addr", r.RemoteAddr).
|
||||||
|
WithField("method", r.Method).
|
||||||
|
WithField("url", r.URL).
|
||||||
|
Info("Serving assets")
|
||||||
|
}
|
||||||
|
assets.Handler().ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/influxdata/chronograf"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CookieExtractor extracts the token from the value of the Name cookie.
|
||||||
|
type CookieExtractor struct {
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract returns the value of cookie Name
|
||||||
|
func (c *CookieExtractor) Extract(r *http.Request) (string, error) {
|
||||||
|
cookie, err := r.Cookie(c.Name)
|
||||||
|
if err != nil {
|
||||||
|
return "", chronograf.ErrAuthentication
|
||||||
|
}
|
||||||
|
return cookie.Value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BearerExtractor extracts the token from Authorization: Bearer header.
|
||||||
|
type BearerExtractor struct{}
|
||||||
|
|
||||||
|
// Extract returns the string following Authorization: Bearer
|
||||||
|
func (b *BearerExtractor) Extract(r *http.Request) (string, error) {
|
||||||
|
s := r.Header.Get("Authorization")
|
||||||
|
if s == "" {
|
||||||
|
return "", chronograf.ErrAuthentication
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for Bearer token.
|
||||||
|
strs := strings.Split(s, " ")
|
||||||
|
|
||||||
|
if len(strs) != 2 || strs[0] != "Bearer" {
|
||||||
|
return "", chronograf.ErrAuthentication
|
||||||
|
}
|
||||||
|
return strs[1], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthorizedToken extracts the token and validates; if valid the next handler
|
||||||
|
// will be run. The principal will be sent to the next handler via the request's
|
||||||
|
// Context. It is up to the next handler to determine if the principal has access.
|
||||||
|
// On failure, will return http.StatusUnauthorized.
|
||||||
|
func AuthorizedToken(auth chronograf.Authenticator, te chronograf.TokenExtractor, logger chronograf.Logger, next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log := logger.
|
||||||
|
WithField("component", "auth").
|
||||||
|
WithField("remote_addr", r.RemoteAddr).
|
||||||
|
WithField("method", r.Method).
|
||||||
|
WithField("url", r.URL)
|
||||||
|
|
||||||
|
token, err := te.Extract(r)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Unable to extract token")
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// We do not check the validity of the principal. Those
|
||||||
|
// handlers further down the chain should do so.
|
||||||
|
principal, err := auth.Authenticate(r.Context(), token)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Invalid token")
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the principal to the next handler
|
||||||
|
ctx := context.WithValue(r.Context(), chronograf.PrincipalKey, principal)
|
||||||
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
return
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,193 @@
|
||||||
|
package handlers_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/influxdata/chronograf"
|
||||||
|
"github.com/influxdata/chronograf/handlers"
|
||||||
|
clog "github.com/influxdata/chronograf/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCookieExtractor(t *testing.T) {
|
||||||
|
var test = []struct {
|
||||||
|
Desc string
|
||||||
|
Name string
|
||||||
|
Value string
|
||||||
|
Lookup string
|
||||||
|
Expected string
|
||||||
|
Err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Desc: "No cookie of this name",
|
||||||
|
Name: "Auth",
|
||||||
|
Value: "reallyimportant",
|
||||||
|
Lookup: "Doesntexist",
|
||||||
|
Expected: "",
|
||||||
|
Err: chronograf.ErrAuthentication,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Desc: "Cookie token extracted",
|
||||||
|
Name: "Auth",
|
||||||
|
Value: "reallyimportant",
|
||||||
|
Lookup: "Auth",
|
||||||
|
Expected: "reallyimportant",
|
||||||
|
Err: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range test {
|
||||||
|
req, _ := http.NewRequest("", "http://howdy.com", nil)
|
||||||
|
req.AddCookie(&http.Cookie{
|
||||||
|
Name: test.Name,
|
||||||
|
Value: test.Value,
|
||||||
|
})
|
||||||
|
|
||||||
|
var e chronograf.TokenExtractor = &handlers.CookieExtractor{
|
||||||
|
Name: test.Lookup,
|
||||||
|
}
|
||||||
|
actual, err := e.Extract(req)
|
||||||
|
if err != test.Err {
|
||||||
|
t.Errorf("Cookie extract error; expected %v actual %v", test.Err, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if actual != test.Expected {
|
||||||
|
t.Errorf("Token extract error; expected %v actual %v", test.Expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBearerExtractor(t *testing.T) {
|
||||||
|
var test = []struct {
|
||||||
|
Desc string
|
||||||
|
Header string
|
||||||
|
Value string
|
||||||
|
Lookup string
|
||||||
|
Expected string
|
||||||
|
Err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Desc: "No header of this name",
|
||||||
|
Header: "Doesntexist",
|
||||||
|
Value: "reallyimportant",
|
||||||
|
Expected: "",
|
||||||
|
Err: chronograf.ErrAuthentication,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Desc: "Auth header doesn't have Bearer",
|
||||||
|
Header: "Authorization",
|
||||||
|
Value: "Bad Value",
|
||||||
|
Expected: "",
|
||||||
|
Err: chronograf.ErrAuthentication,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Desc: "Auth header doesn't have Bearer token",
|
||||||
|
Header: "Authorization",
|
||||||
|
Value: "Bearer",
|
||||||
|
Expected: "",
|
||||||
|
Err: chronograf.ErrAuthentication,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Desc: "Authorization Bearer token success",
|
||||||
|
Header: "Authorization",
|
||||||
|
Value: "Bearer howdy",
|
||||||
|
Expected: "howdy",
|
||||||
|
Err: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range test {
|
||||||
|
req, _ := http.NewRequest("", "http://howdy.com", nil)
|
||||||
|
req.Header.Add(test.Header, test.Value)
|
||||||
|
|
||||||
|
var e chronograf.TokenExtractor = &handlers.BearerExtractor{}
|
||||||
|
actual, err := e.Extract(req)
|
||||||
|
if err != test.Err {
|
||||||
|
t.Errorf("Bearer extract error; expected %v actual %v", test.Err, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if actual != test.Expected {
|
||||||
|
t.Errorf("Token extract error; expected %v actual %v", test.Expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockExtractor struct {
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockExtractor) Extract(*http.Request) (string, error) {
|
||||||
|
return "", m.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockAuthenticator struct {
|
||||||
|
Principal chronograf.Principal
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockAuthenticator) Authenticate(context.Context, string) (chronograf.Principal, error) {
|
||||||
|
return m.Principal, m.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockAuthenticator) Token(context.Context, chronograf.Principal, time.Duration) (string, error) {
|
||||||
|
return "", m.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthorizedToken(t *testing.T) {
|
||||||
|
var tests = []struct {
|
||||||
|
Desc string
|
||||||
|
Code int
|
||||||
|
Principal chronograf.Principal
|
||||||
|
ExtractorErr error
|
||||||
|
AuthErr error
|
||||||
|
Expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Desc: "Error in extractor",
|
||||||
|
Code: http.StatusUnauthorized,
|
||||||
|
ExtractorErr: errors.New("error"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Desc: "Error in extractor",
|
||||||
|
Code: http.StatusUnauthorized,
|
||||||
|
AuthErr: errors.New("error"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Desc: "Authorized ok",
|
||||||
|
Code: http.StatusOK,
|
||||||
|
Principal: "Principal Strickland",
|
||||||
|
Expected: "Principal Strickland",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
// next is a sentinel StatusOK and
|
||||||
|
// principal recorder.
|
||||||
|
var principal chronograf.Principal
|
||||||
|
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
principal = r.Context().Value(chronograf.PrincipalKey).(chronograf.Principal)
|
||||||
|
})
|
||||||
|
req, _ := http.NewRequest("GET", "", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
e := &MockExtractor{
|
||||||
|
Err: test.ExtractorErr,
|
||||||
|
}
|
||||||
|
a := &MockAuthenticator{
|
||||||
|
Err: test.AuthErr,
|
||||||
|
Principal: test.Principal,
|
||||||
|
}
|
||||||
|
|
||||||
|
logger := clog.New()
|
||||||
|
handler := handlers.AuthorizedToken(a, e, logger, next)
|
||||||
|
handler.ServeHTTP(w, req)
|
||||||
|
if w.Code != test.Code {
|
||||||
|
t.Errorf("Status code expected: %d actual %d", test.Code, w.Code)
|
||||||
|
} else if principal != test.Principal {
|
||||||
|
t.Errorf("Principal mismatch expected: %s actual %s", test.Principal, principal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,208 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/go-github/github"
|
||||||
|
"github.com/influxdata/chronograf"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
ogh "golang.org/x/oauth2/github"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultCookieName = "session"
|
||||||
|
DefaultCookieDuration = time.Hour * 24 * 30
|
||||||
|
)
|
||||||
|
|
||||||
|
// Cookie represents the location and expiration time of new cookies.
|
||||||
|
type Cookie struct {
|
||||||
|
Name string
|
||||||
|
Duration time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCookie creates a Cookie with DefaultCookieName and DefaultCookieDuration
|
||||||
|
func NewCookie() Cookie {
|
||||||
|
return Cookie{
|
||||||
|
Name: DefaultCookieName,
|
||||||
|
Duration: DefaultCookieDuration,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Github provides OAuth Login and Callback handlers. Callback will set
|
||||||
|
// an authentication cookie. This cookie's value is a JWT containing
|
||||||
|
// the user's primary Github email address.
|
||||||
|
type Github struct {
|
||||||
|
Cookie Cookie
|
||||||
|
Authenticator chronograf.Authenticator
|
||||||
|
ClientID string
|
||||||
|
ClientSecret string
|
||||||
|
Scopes []string
|
||||||
|
SuccessURL string // SuccessURL is redirect location after successful authorization
|
||||||
|
FailureURL string // FailureURL is redirect location after authorization failure
|
||||||
|
Now func() time.Time
|
||||||
|
Logger chronograf.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGithub constructs a Github with default cookie behavior and scopes.
|
||||||
|
func NewGithub(clientID, clientSecret, successURL, failureURL string, auth chronograf.Authenticator, log chronograf.Logger) Github {
|
||||||
|
return Github{
|
||||||
|
ClientID: clientID,
|
||||||
|
ClientSecret: clientSecret,
|
||||||
|
Cookie: NewCookie(),
|
||||||
|
Scopes: []string{"user:email"},
|
||||||
|
SuccessURL: successURL,
|
||||||
|
FailureURL: failureURL,
|
||||||
|
Authenticator: auth,
|
||||||
|
Now: time.Now,
|
||||||
|
Logger: log,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Github) config() *oauth2.Config {
|
||||||
|
return &oauth2.Config{
|
||||||
|
ClientID: g.ClientID,
|
||||||
|
ClientSecret: g.ClientSecret,
|
||||||
|
Scopes: g.Scopes,
|
||||||
|
Endpoint: ogh.Endpoint,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login returns a handler that redirects to Github's OAuth login.
|
||||||
|
// Uses JWT with a random string as the state validation method.
|
||||||
|
// JWTs are used because they can be validated without storing
|
||||||
|
// state.
|
||||||
|
func (g *Github) Login() http.Handler {
|
||||||
|
conf := g.config()
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// We are creating a token with an encoded random string to prevent CSRF attacks
|
||||||
|
// This token will be validated during the OAuth callback.
|
||||||
|
// We'll give our users 10 minutes from this point to type in their github password.
|
||||||
|
// If the callback is not received within 10 minutes, then authorization will fail.
|
||||||
|
csrf := randomString(32) // 32 is not important... just long
|
||||||
|
state, err := g.Authenticator.Token(r.Context(), chronograf.Principal(csrf), 10*time.Minute)
|
||||||
|
// This is likely an internal server error
|
||||||
|
if err != nil {
|
||||||
|
g.Logger.
|
||||||
|
WithField("component", "auth").
|
||||||
|
WithField("remote_addr", r.RemoteAddr).
|
||||||
|
WithField("method", r.Method).
|
||||||
|
WithField("url", r.URL).
|
||||||
|
Error("Internal authentication error: ", err.Error())
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
url := conf.AuthCodeURL(state, oauth2.AccessTypeOnline)
|
||||||
|
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logout will expire our authentication cookie and redirect to the SuccessURL
|
||||||
|
func (g *Github) Logout() http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
deleteCookie := http.Cookie{
|
||||||
|
Name: g.Cookie.Name,
|
||||||
|
Value: "none",
|
||||||
|
Expires: g.Now().Add(-1 * time.Hour),
|
||||||
|
HttpOnly: true,
|
||||||
|
Path: "/",
|
||||||
|
}
|
||||||
|
http.SetCookie(w, &deleteCookie)
|
||||||
|
http.Redirect(w, r, g.SuccessURL, http.StatusTemporaryRedirect)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback used by github callback after authorization is granted. If
|
||||||
|
// granted, Callback will set a cookie with a month-long expiration. The
|
||||||
|
// value of the cookie is a JWT because the JWT can be validated without
|
||||||
|
// the need for saving state. The JWT contains the Github user's primary
|
||||||
|
// email address.
|
||||||
|
func (g *Github) Callback() http.Handler {
|
||||||
|
conf := g.config()
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log := g.Logger.
|
||||||
|
WithField("component", "auth").
|
||||||
|
WithField("remote_addr", r.RemoteAddr).
|
||||||
|
WithField("method", r.Method).
|
||||||
|
WithField("url", r.URL)
|
||||||
|
|
||||||
|
state := r.FormValue("state")
|
||||||
|
// Check if the OAuth state token is valid to prevent CSRF
|
||||||
|
_, err := g.Authenticator.Authenticate(r.Context(), state)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Invalid OAuth state received: ", err.Error())
|
||||||
|
http.Redirect(w, r, g.FailureURL, http.StatusTemporaryRedirect)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
code := r.FormValue("code")
|
||||||
|
token, err := conf.Exchange(r.Context(), code)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Unable to exchange code for token ", err.Error())
|
||||||
|
http.Redirect(w, r, g.FailureURL, http.StatusTemporaryRedirect)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
oauthClient := conf.Client(r.Context(), token)
|
||||||
|
client := github.NewClient(oauthClient)
|
||||||
|
|
||||||
|
emails, resp, err := client.Users.ListEmails(nil)
|
||||||
|
if err != nil {
|
||||||
|
switch resp.StatusCode {
|
||||||
|
case http.StatusUnauthorized, http.StatusForbidden:
|
||||||
|
log.Error("OAuth access to email address forbidden ", err.Error())
|
||||||
|
default:
|
||||||
|
log.Error("Unable to retrieve Github email ", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, g.FailureURL, http.StatusTemporaryRedirect)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
email, err := primaryEmail(emails)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Unable to retrieve primary Github email ", err.Error())
|
||||||
|
http.Redirect(w, r, g.FailureURL, http.StatusTemporaryRedirect)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We create an auth token that will be used by all other endpoints to validate the principal has a claim
|
||||||
|
authToken, err := g.Authenticator.Token(r.Context(), chronograf.Principal(email), g.Cookie.Duration)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Unable to create cookie auth token ", err.Error())
|
||||||
|
http.Redirect(w, r, g.FailureURL, http.StatusTemporaryRedirect)
|
||||||
|
}
|
||||||
|
|
||||||
|
expireCookie := time.Now().Add(g.Cookie.Duration)
|
||||||
|
cookie := http.Cookie{
|
||||||
|
Name: g.Cookie.Name,
|
||||||
|
Value: authToken,
|
||||||
|
Expires: expireCookie,
|
||||||
|
HttpOnly: true,
|
||||||
|
Path: "/",
|
||||||
|
}
|
||||||
|
log.Info("User ", email, " is authenticated")
|
||||||
|
http.SetCookie(w, &cookie)
|
||||||
|
http.Redirect(w, r, g.SuccessURL, http.StatusTemporaryRedirect)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func randomString(length int) string {
|
||||||
|
k := make([]byte, length)
|
||||||
|
if _, err := io.ReadFull(rand.Reader, k); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return base64.StdEncoding.EncodeToString(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
func primaryEmail(emails []*github.UserEmail) (string, error) {
|
||||||
|
for _, m := range emails {
|
||||||
|
if m != nil && m.Primary != nil && m.Verified != nil && m.Email != nil {
|
||||||
|
return *m.Email, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", errors.New("No primary email address")
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
package jwt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
gojwt "github.com/dgrijalva/jwt-go"
|
||||||
|
"github.com/influxdata/chronograf"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test if JWT implements Authenticator
|
||||||
|
var _ chronograf.Authenticator = &JWT{}
|
||||||
|
|
||||||
|
// JWT represents a javascript web token that can be validated or marshaled into string.
|
||||||
|
type JWT struct {
|
||||||
|
Secret string
|
||||||
|
Now func() time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewJWT creates a new JWT using time.Now; secret is used for signing and validating.
|
||||||
|
func NewJWT(secret string) JWT {
|
||||||
|
return JWT{
|
||||||
|
Secret: secret,
|
||||||
|
Now: time.Now,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure Claims implements the jwt.Claims interface
|
||||||
|
var _ gojwt.Claims = &Claims{}
|
||||||
|
|
||||||
|
// Claims extends jwt.StandardClaims Valid to make sure claims has a subject.
|
||||||
|
type Claims struct {
|
||||||
|
gojwt.StandardClaims
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid adds an empty subject test to the StandardClaims checks.
|
||||||
|
func (c *Claims) Valid() error {
|
||||||
|
if err := c.StandardClaims.Valid(); err != nil {
|
||||||
|
return err
|
||||||
|
} else if c.StandardClaims.Subject == "" {
|
||||||
|
return fmt.Errorf("claim has no subject")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate checks if the jwtToken is signed correctly and validates with Claims.
|
||||||
|
func (j *JWT) Authenticate(ctx context.Context, jwtToken string) (chronograf.Principal, error) {
|
||||||
|
gojwt.TimeFunc = j.Now
|
||||||
|
|
||||||
|
// Check for expected signing method.
|
||||||
|
alg := func(token *gojwt.Token) (interface{}, error) {
|
||||||
|
if _, ok := token.Method.(*gojwt.SigningMethodHMAC); !ok {
|
||||||
|
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||||
|
}
|
||||||
|
return []byte(j.Secret), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Checks for expired tokens
|
||||||
|
// 2. Checks if time is after the issued at
|
||||||
|
// 3. Check if time is after not before (nbf)
|
||||||
|
// 4. Check if subject is not empty
|
||||||
|
token, err := gojwt.ParseWithClaims(jwtToken, &Claims{}, alg)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
} else if !token.Valid {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := token.Claims.(*Claims)
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("unable to convert claims to standard claims")
|
||||||
|
}
|
||||||
|
|
||||||
|
return chronograf.Principal(claims.Subject), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token creates a signed JWT token from user that expires at Now + duration
|
||||||
|
func (j *JWT) Token(ctx context.Context, user chronograf.Principal, duration time.Duration) (string, error) {
|
||||||
|
// Create a new token object, specifying signing method and the claims
|
||||||
|
// you would like it to contain.
|
||||||
|
now := j.Now()
|
||||||
|
claims := &Claims{
|
||||||
|
gojwt.StandardClaims{
|
||||||
|
Subject: string(user),
|
||||||
|
ExpiresAt: now.Add(duration).Unix(),
|
||||||
|
IssuedAt: now.Unix(),
|
||||||
|
NotBefore: now.Unix(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
token := gojwt.NewWithClaims(gojwt.SigningMethodHS256, claims)
|
||||||
|
|
||||||
|
// Sign and get the complete encoded token as a string using the secret
|
||||||
|
return token.SignedString([]byte(j.Secret))
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
package jwt_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/influxdata/chronograf"
|
||||||
|
"github.com/influxdata/chronograf/jwt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAuthenticate(t *testing.T) {
|
||||||
|
var tests = []struct {
|
||||||
|
Desc string
|
||||||
|
Secret string
|
||||||
|
Token string
|
||||||
|
User chronograf.Principal
|
||||||
|
Err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Desc: "Test bad jwt token",
|
||||||
|
Secret: "secret",
|
||||||
|
Token: "badtoken",
|
||||||
|
User: "",
|
||||||
|
Err: errors.New("token contains an invalid number of segments"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Desc: "Test valid jwt token",
|
||||||
|
Secret: "secret",
|
||||||
|
Token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIvY2hyb25vZ3JhZi92MS91c2Vycy8xIiwibmFtZSI6IkRvYyBCcm93biIsImlhdCI6LTQ0Njc3NDQwMCwiZXhwIjotNDQ2Nzc0NDAwLCJuYmYiOi00NDY3NzQ0MDB9._rZ4gOIei9PizHOABH6kLcJTA3jm8ls0YnDxtz1qeUI",
|
||||||
|
User: "/chronograf/v1/users/1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Desc: "Test expired jwt token",
|
||||||
|
Secret: "secret",
|
||||||
|
Token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIvY2hyb25vZ3JhZi92MS91c2Vycy8xIiwibmFtZSI6IkRvYyBCcm93biIsImlhdCI6LTQ0Njc3NDQwMCwiZXhwIjotNDQ2Nzc0NDAxLCJuYmYiOi00NDY3NzQ0MDB9.vWXdm0-XQ_pW62yBpSISFFJN_yz0vqT9_INcUKTp5Q8",
|
||||||
|
User: "",
|
||||||
|
Err: errors.New("token is expired by 1s"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Desc: "Test jwt token not before time",
|
||||||
|
Secret: "secret",
|
||||||
|
Token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIvY2hyb25vZ3JhZi92MS91c2Vycy8xIiwibmFtZSI6IkRvYyBCcm93biIsImlhdCI6LTQ0Njc3NDQwMCwiZXhwIjotNDQ2Nzc0NDAwLCJuYmYiOi00NDY3NzQzOTl9.TMGAhv57u1aosjc4ywKC7cElP1tKyQH7GmRF2ToAxlE",
|
||||||
|
User: "",
|
||||||
|
Err: errors.New("token is not valid yet"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Desc: "Test jwt with empty subject is invalid",
|
||||||
|
Secret: "secret",
|
||||||
|
Token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOi00NDY3NzQ0MDAsImV4cCI6LTQ0Njc3NDQwMCwibmJmIjotNDQ2Nzc0NDAwfQ.gxsA6_Ei3s0f2I1TAtrrb8FmGiO25OqVlktlF_ylhX4",
|
||||||
|
User: "",
|
||||||
|
Err: errors.New("claim has no subject"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for i, test := range tests {
|
||||||
|
j := jwt.JWT{
|
||||||
|
Secret: test.Secret,
|
||||||
|
Now: func() time.Time {
|
||||||
|
return time.Unix(-446774400, 0)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
user, err := j.Authenticate(context.Background(), test.Token)
|
||||||
|
if err != nil {
|
||||||
|
if test.Err == nil {
|
||||||
|
t.Errorf("Error in test %d authenticating with bad token: %v", i, err)
|
||||||
|
} else if err.Error() != test.Err.Error() {
|
||||||
|
t.Errorf("Error in test %d expected error: %v actual: %v", i, err, test.Err)
|
||||||
|
}
|
||||||
|
} else if test.User != user {
|
||||||
|
t.Errorf("Error in test %d; users different; expected: %v actual: %v", i, test.User, user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToken(t *testing.T) {
|
||||||
|
duration := time.Second
|
||||||
|
expected := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOi00NDY3NzQzOTksImlhdCI6LTQ0Njc3NDQwMCwibmJmIjotNDQ2Nzc0NDAwLCJzdWIiOiIvY2hyb25vZ3JhZi92MS91c2Vycy8xIn0.ofQM6yTmrmve5JeEE0RcK4_euLXuZ_rdh6bLAbtbC9M"
|
||||||
|
j := jwt.JWT{
|
||||||
|
Secret: "secret",
|
||||||
|
Now: func() time.Time {
|
||||||
|
return time.Unix(-446774400, 0)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if token, err := j.Token(context.Background(), chronograf.Principal("/chronograf/v1/users/1"), duration); err != nil {
|
||||||
|
t.Errorf("Error creating token for user: %v", err)
|
||||||
|
} else if token != expected {
|
||||||
|
t.Errorf("Error creating token; expected: %s actual: %s", "", token)
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
jwt "github.com/dgrijalva/jwt-go"
|
||||||
"github.com/go-openapi/runtime/middleware"
|
"github.com/go-openapi/runtime/middleware"
|
||||||
"github.com/go-openapi/strfmt"
|
"github.com/go-openapi/strfmt"
|
||||||
"github.com/influxdata/chronograf"
|
"github.com/influxdata/chronograf"
|
||||||
|
@ -363,3 +364,26 @@ func (m *Handler) GetMappings(ctx context.Context, params op.GetMappingsParams)
|
||||||
}
|
}
|
||||||
return op.NewGetMappingsOK().WithPayload(mp)
|
return op.NewGetMappingsOK().WithPayload(mp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Handler) Token(ctx context.Context, params op.GetTokenParams) middleware.Responder {
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS512, jwt.MapClaims{
|
||||||
|
"sub": "bob",
|
||||||
|
"exp": time.Now().Add(time.Hour * 24 * 30).Unix(),
|
||||||
|
"username": "bob",
|
||||||
|
"email": "bob@mail.com",
|
||||||
|
"nbf": time.Now().Unix(),
|
||||||
|
"iat": time.Now().Unix(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// sign token with secret
|
||||||
|
ts, err := token.SignedString([]byte("secret"))
|
||||||
|
if err != nil {
|
||||||
|
errMsg := &models.Error{Code: 500, Message: "Failed to sign token"}
|
||||||
|
return op.NewGetTokenDefault(500).WithPayload(errMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
t := models.Token(ts)
|
||||||
|
|
||||||
|
return op.NewGetTokenOK().WithPayload(t)
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
// This file was generated by the swagger tool.
|
||||||
|
// Editing this file might prove futile when you re-run the swagger generate command
|
||||||
|
|
||||||
|
// Token a stringified JWT token.
|
||||||
|
// swagger:model Token
|
||||||
|
type Token interface{}
|
|
@ -15,9 +15,9 @@ import (
|
||||||
"github.com/influxdata/chronograf"
|
"github.com/influxdata/chronograf"
|
||||||
"github.com/influxdata/chronograf/bolt"
|
"github.com/influxdata/chronograf/bolt"
|
||||||
"github.com/influxdata/chronograf/canned"
|
"github.com/influxdata/chronograf/canned"
|
||||||
"github.com/influxdata/chronograf/dist"
|
|
||||||
"github.com/influxdata/chronograf/handlers"
|
"github.com/influxdata/chronograf/handlers"
|
||||||
"github.com/influxdata/chronograf/influx"
|
"github.com/influxdata/chronograf/influx"
|
||||||
|
"github.com/influxdata/chronograf/jwt"
|
||||||
"github.com/influxdata/chronograf/kapacitor"
|
"github.com/influxdata/chronograf/kapacitor"
|
||||||
"github.com/influxdata/chronograf/layouts"
|
"github.com/influxdata/chronograf/layouts"
|
||||||
clog "github.com/influxdata/chronograf/log"
|
clog "github.com/influxdata/chronograf/log"
|
||||||
|
@ -30,7 +30,7 @@ import (
|
||||||
|
|
||||||
//go:generate swagger generate server --target .. --name --spec ../swagger.yaml --with-context
|
//go:generate swagger generate server --target .. --name --spec ../swagger.yaml --with-context
|
||||||
|
|
||||||
var logger = clog.New()
|
var logger chronograf.Logger = clog.New()
|
||||||
|
|
||||||
var devFlags = struct {
|
var devFlags = struct {
|
||||||
Develop bool `short:"d" long:"develop" description:"Run server in develop mode."`
|
Develop bool `short:"d" long:"develop" description:"Run server in develop mode."`
|
||||||
|
@ -44,6 +44,12 @@ var cannedFlags = struct {
|
||||||
CannedPath string `short:"c" long:"canned-path" description:"Path to directory of pre-canned application layouts" env:"CANNED_PATH" default:"canned"`
|
CannedPath string `short:"c" long:"canned-path" description:"Path to directory of pre-canned application layouts" env:"CANNED_PATH" default:"canned"`
|
||||||
}{}
|
}{}
|
||||||
|
|
||||||
|
var authFlags = struct {
|
||||||
|
TokenSecret string `short:"t" long:"token-secret" description:"Secret to sign tokens" env:"TOKEN_SECRET"`
|
||||||
|
GithubClientID string `short:"i" long:"github-client-id" description:"Github Client ID for OAuth 2 support" env:"GH_CLIENT_ID"`
|
||||||
|
GithubClientSecret string `short:"s" long:"github-client-secret" description:"Github Client Secret for OAuth 2 support" env:"GH_CLIENT_SECRET"`
|
||||||
|
}{}
|
||||||
|
|
||||||
func configureFlags(api *op.ChronografAPI) {
|
func configureFlags(api *op.ChronografAPI) {
|
||||||
api.CommandLineOptionsGroups = []swag.CommandLineOptionsGroup{
|
api.CommandLineOptionsGroups = []swag.CommandLineOptionsGroup{
|
||||||
swag.CommandLineOptionsGroup{
|
swag.CommandLineOptionsGroup{
|
||||||
|
@ -61,19 +67,11 @@ func configureFlags(api *op.ChronografAPI) {
|
||||||
LongDescription: "Specify the path to a directory of pre-canned application layout files.",
|
LongDescription: "Specify the path to a directory of pre-canned application layout files.",
|
||||||
Options: &cannedFlags,
|
Options: &cannedFlags,
|
||||||
},
|
},
|
||||||
}
|
swag.CommandLineOptionsGroup{
|
||||||
}
|
ShortDescription: "Server Authentication",
|
||||||
|
LongDescription: "Server will use authentication",
|
||||||
func assets() chronograf.Assets {
|
Options: &authFlags,
|
||||||
if devFlags.Develop {
|
},
|
||||||
return &dist.DebugAssets{
|
|
||||||
Dir: "ui/build",
|
|
||||||
Default: "ui/build/index.html",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &dist.BindataAssets{
|
|
||||||
Prefix: "ui/build",
|
|
||||||
Default: "ui/build/index.html",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,6 +101,7 @@ func configureAPI(api *op.ChronografAPI) http.Handler {
|
||||||
c := bolt.NewClient()
|
c := bolt.NewClient()
|
||||||
c.Path = storeFlags.BoltPath
|
c.Path = storeFlags.BoltPath
|
||||||
if err := c.Open(); err != nil {
|
if err := c.Open(); err != nil {
|
||||||
|
logger.WithField("component", "boltstore").Panic("Unable to open boltdb; is there a mrfusion already running?", err)
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,6 +121,7 @@ func configureAPI(api *op.ChronografAPI) http.Handler {
|
||||||
LayoutStore: allLayouts,
|
LayoutStore: allLayouts,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
api.GetTokenHandler = op.GetTokenHandlerFunc(mockHandler.Token)
|
||||||
api.DeleteSourcesIDUsersUserIDExplorationsExplorationIDHandler = op.DeleteSourcesIDUsersUserIDExplorationsExplorationIDHandlerFunc(h.DeleteExploration)
|
api.DeleteSourcesIDUsersUserIDExplorationsExplorationIDHandler = op.DeleteSourcesIDUsersUserIDExplorationsExplorationIDHandlerFunc(h.DeleteExploration)
|
||||||
api.GetSourcesIDUsersUserIDExplorationsExplorationIDHandler = op.GetSourcesIDUsersUserIDExplorationsExplorationIDHandlerFunc(h.Exploration)
|
api.GetSourcesIDUsersUserIDExplorationsExplorationIDHandler = op.GetSourcesIDUsersUserIDExplorationsExplorationIDHandlerFunc(h.Exploration)
|
||||||
api.GetSourcesIDUsersUserIDExplorationsHandler = op.GetSourcesIDUsersUserIDExplorationsHandlerFunc(h.Explorations)
|
api.GetSourcesIDUsersUserIDExplorationsHandler = op.GetSourcesIDUsersUserIDExplorationsHandlerFunc(h.Explorations)
|
||||||
|
@ -238,6 +238,38 @@ func setupMiddlewares(handler http.Handler) http.Handler {
|
||||||
// The middleware configuration happens before anything, this middleware also applies to serving the swagger.json document.
|
// The middleware configuration happens before anything, this middleware also applies to serving the swagger.json document.
|
||||||
// So this is a good place to plug in a panic handling middleware, logging and metrics
|
// So this is a good place to plug in a panic handling middleware, logging and metrics
|
||||||
func setupGlobalMiddleware(handler http.Handler) http.Handler {
|
func setupGlobalMiddleware(handler http.Handler) http.Handler {
|
||||||
|
successURL := "/"
|
||||||
|
failureURL := "/login"
|
||||||
|
|
||||||
|
// TODO: Fix these routes when we use httprouter
|
||||||
|
assets := handlers.Assets(handlers.AssetsOpts{
|
||||||
|
Develop: devFlags.Develop,
|
||||||
|
Logger: logger,
|
||||||
|
})
|
||||||
|
|
||||||
|
if authFlags.TokenSecret != "" {
|
||||||
|
e := handlers.CookieExtractor{
|
||||||
|
Name: "session",
|
||||||
|
}
|
||||||
|
a := jwt.NewJWT(authFlags.TokenSecret)
|
||||||
|
handler = handlers.AuthorizedToken(&a, &e, logger, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Fix these routes when we use httprouter
|
||||||
|
auth := jwt.NewJWT(authFlags.TokenSecret)
|
||||||
|
gh := handlers.NewGithub(
|
||||||
|
authFlags.GithubClientID,
|
||||||
|
authFlags.GithubClientSecret,
|
||||||
|
successURL,
|
||||||
|
failureURL,
|
||||||
|
&auth,
|
||||||
|
logger,
|
||||||
|
)
|
||||||
|
|
||||||
|
login := gh.Login()
|
||||||
|
logout := gh.Logout()
|
||||||
|
callback := gh.Callback()
|
||||||
|
|
||||||
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
l := logger.
|
l := logger.
|
||||||
WithField("component", "server").
|
WithField("component", "server").
|
||||||
|
@ -245,17 +277,29 @@ func setupGlobalMiddleware(handler http.Handler) http.Handler {
|
||||||
WithField("method", r.Method).
|
WithField("method", r.Method).
|
||||||
WithField("url", r.URL)
|
WithField("url", r.URL)
|
||||||
|
|
||||||
|
// TODO: Warning keep these paths in this order until
|
||||||
|
// we have a real router.
|
||||||
if strings.Contains(r.URL.Path, "/chronograf/v1") {
|
if strings.Contains(r.URL.Path, "/chronograf/v1") {
|
||||||
l.Info("Serving API Request")
|
l.Info("Serving API Request")
|
||||||
handler.ServeHTTP(w, r)
|
handler.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
|
} else if strings.Contains(r.URL.Path, "/oauth/github/callback") {
|
||||||
|
l.Info("Auth callback")
|
||||||
|
callback.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
} else if strings.HasPrefix(r.URL.Path, "/oauth/logout") {
|
||||||
|
l.Info("Login request")
|
||||||
|
logout.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
} else if strings.HasPrefix(r.URL.Path, "/oauth") {
|
||||||
|
l.Info("Login request")
|
||||||
|
login.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
} else if r.URL.Path == "//" {
|
} else if r.URL.Path == "//" {
|
||||||
l.Info("Serving root redirect")
|
l.Info("Serving root redirect")
|
||||||
http.Redirect(w, r, "/index.html", http.StatusFound)
|
http.Redirect(w, r, "/index.html", http.StatusFound)
|
||||||
} else {
|
} else {
|
||||||
l.Info("Serving assets")
|
assets.ServeHTTP(w, r)
|
||||||
assets().Handler().ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// TODO: When we use httprouter clean up these routes
|
// TODO: When we use httprouter clean up these routes
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -91,6 +91,8 @@ type ChronografAPI struct {
|
||||||
GetSourcesIDUsersUserIDExplorationsHandler GetSourcesIDUsersUserIDExplorationsHandler
|
GetSourcesIDUsersUserIDExplorationsHandler GetSourcesIDUsersUserIDExplorationsHandler
|
||||||
// GetSourcesIDUsersUserIDExplorationsExplorationIDHandler sets the operation handler for the get sources ID users user ID explorations exploration ID operation
|
// GetSourcesIDUsersUserIDExplorationsExplorationIDHandler sets the operation handler for the get sources ID users user ID explorations exploration ID operation
|
||||||
GetSourcesIDUsersUserIDExplorationsExplorationIDHandler GetSourcesIDUsersUserIDExplorationsExplorationIDHandler
|
GetSourcesIDUsersUserIDExplorationsExplorationIDHandler GetSourcesIDUsersUserIDExplorationsExplorationIDHandler
|
||||||
|
// GetTokenHandler sets the operation handler for the get token operation
|
||||||
|
GetTokenHandler GetTokenHandler
|
||||||
// PatchSourcesIDHandler sets the operation handler for the patch sources ID operation
|
// PatchSourcesIDHandler sets the operation handler for the patch sources ID operation
|
||||||
PatchSourcesIDHandler PatchSourcesIDHandler
|
PatchSourcesIDHandler PatchSourcesIDHandler
|
||||||
// PatchSourcesIDKapacitorsKapaIDHandler sets the operation handler for the patch sources ID kapacitors kapa ID operation
|
// PatchSourcesIDKapacitorsKapaIDHandler sets the operation handler for the patch sources ID kapacitors kapa ID operation
|
||||||
|
@ -280,6 +282,10 @@ func (o *ChronografAPI) Validate() error {
|
||||||
unregistered = append(unregistered, "GetSourcesIDUsersUserIDExplorationsExplorationIDHandler")
|
unregistered = append(unregistered, "GetSourcesIDUsersUserIDExplorationsExplorationIDHandler")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if o.GetTokenHandler == nil {
|
||||||
|
unregistered = append(unregistered, "GetTokenHandler")
|
||||||
|
}
|
||||||
|
|
||||||
if o.PatchSourcesIDHandler == nil {
|
if o.PatchSourcesIDHandler == nil {
|
||||||
unregistered = append(unregistered, "PatchSourcesIDHandler")
|
unregistered = append(unregistered, "PatchSourcesIDHandler")
|
||||||
}
|
}
|
||||||
|
@ -540,6 +546,11 @@ func (o *ChronografAPI) initHandlerCache() {
|
||||||
}
|
}
|
||||||
o.handlers["GET"]["/sources/{id}/users/{user_id}/explorations/{exploration_id}"] = NewGetSourcesIDUsersUserIDExplorationsExplorationID(o.context, o.GetSourcesIDUsersUserIDExplorationsExplorationIDHandler)
|
o.handlers["GET"]["/sources/{id}/users/{user_id}/explorations/{exploration_id}"] = NewGetSourcesIDUsersUserIDExplorationsExplorationID(o.context, o.GetSourcesIDUsersUserIDExplorationsExplorationIDHandler)
|
||||||
|
|
||||||
|
if o.handlers["GET"] == nil {
|
||||||
|
o.handlers[strings.ToUpper("GET")] = make(map[string]http.Handler)
|
||||||
|
}
|
||||||
|
o.handlers["GET"]["/token"] = NewGetToken(o.context, o.GetTokenHandler)
|
||||||
|
|
||||||
if o.handlers["PATCH"] == nil {
|
if o.handlers["PATCH"] == nil {
|
||||||
o.handlers[strings.ToUpper("PATCH")] = make(map[string]http.Handler)
|
o.handlers[strings.ToUpper("PATCH")] = make(map[string]http.Handler)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
package operations
|
||||||
|
|
||||||
|
// This file was generated by the swagger tool.
|
||||||
|
// Editing this file might prove futile when you re-run the generate command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
context "golang.org/x/net/context"
|
||||||
|
|
||||||
|
middleware "github.com/go-openapi/runtime/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetTokenHandlerFunc turns a function with the right signature into a get token handler
|
||||||
|
type GetTokenHandlerFunc func(context.Context, GetTokenParams) middleware.Responder
|
||||||
|
|
||||||
|
// Handle executing the request and returning a response
|
||||||
|
func (fn GetTokenHandlerFunc) Handle(ctx context.Context, params GetTokenParams) middleware.Responder {
|
||||||
|
return fn(ctx, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTokenHandler interface for that can handle valid get token params
|
||||||
|
type GetTokenHandler interface {
|
||||||
|
Handle(context.Context, GetTokenParams) middleware.Responder
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGetToken creates a new http.Handler for the get token operation
|
||||||
|
func NewGetToken(ctx *middleware.Context, handler GetTokenHandler) *GetToken {
|
||||||
|
return &GetToken{Context: ctx, Handler: handler}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*GetToken swagger:route GET /token getToken
|
||||||
|
|
||||||
|
Authentication token
|
||||||
|
|
||||||
|
Generates a JWT authentication token
|
||||||
|
|
||||||
|
|
||||||
|
*/
|
||||||
|
type GetToken struct {
|
||||||
|
Context *middleware.Context
|
||||||
|
Handler GetTokenHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *GetToken) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
route, _ := o.Context.RouteInfo(r)
|
||||||
|
var Params = NewGetTokenParams()
|
||||||
|
|
||||||
|
if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params
|
||||||
|
o.Context.Respond(rw, r, route.Produces, route, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res := o.Handler.Handle(context.Background(), Params) // actually handle the request
|
||||||
|
|
||||||
|
o.Context.Respond(rw, r, route.Produces, route, res)
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package operations
|
||||||
|
|
||||||
|
// This file was generated by the swagger tool.
|
||||||
|
// Editing this file might prove futile when you re-run the swagger generate command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/go-openapi/errors"
|
||||||
|
"github.com/go-openapi/runtime/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewGetTokenParams creates a new GetTokenParams object
|
||||||
|
// with the default values initialized.
|
||||||
|
func NewGetTokenParams() GetTokenParams {
|
||||||
|
var ()
|
||||||
|
return GetTokenParams{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTokenParams contains all the bound params for the get token operation
|
||||||
|
// typically these are obtained from a http.Request
|
||||||
|
//
|
||||||
|
// swagger:parameters GetToken
|
||||||
|
type GetTokenParams struct {
|
||||||
|
|
||||||
|
// HTTP Request Object
|
||||||
|
HTTPRequest *http.Request
|
||||||
|
}
|
||||||
|
|
||||||
|
// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface
|
||||||
|
// for simple values it will use straight method calls
|
||||||
|
func (o *GetTokenParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {
|
||||||
|
var res []error
|
||||||
|
o.HTTPRequest = r
|
||||||
|
|
||||||
|
if len(res) > 0 {
|
||||||
|
return errors.CompositeValidationError(res...)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
package operations
|
||||||
|
|
||||||
|
// This file was generated by the swagger tool.
|
||||||
|
// Editing this file might prove futile when you re-run the swagger generate command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/go-openapi/runtime"
|
||||||
|
|
||||||
|
"github.com/influxdata/chronograf/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*GetTokenOK A JWT authentication token
|
||||||
|
|
||||||
|
swagger:response getTokenOK
|
||||||
|
*/
|
||||||
|
type GetTokenOK struct {
|
||||||
|
|
||||||
|
// In: body
|
||||||
|
Payload models.Token `json:"body,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGetTokenOK creates GetTokenOK with default headers values
|
||||||
|
func NewGetTokenOK() *GetTokenOK {
|
||||||
|
return &GetTokenOK{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithPayload adds the payload to the get token o k response
|
||||||
|
func (o *GetTokenOK) WithPayload(payload models.Token) *GetTokenOK {
|
||||||
|
o.Payload = payload
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPayload sets the payload to the get token o k response
|
||||||
|
func (o *GetTokenOK) SetPayload(payload models.Token) {
|
||||||
|
o.Payload = payload
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteResponse to the client
|
||||||
|
func (o *GetTokenOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
|
||||||
|
|
||||||
|
rw.WriteHeader(200)
|
||||||
|
if err := producer.Produce(rw, o.Payload); err != nil {
|
||||||
|
panic(err) // let the recovery middleware deal with this
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/*GetTokenDefault Unexpected internal service error
|
||||||
|
|
||||||
|
swagger:response getTokenDefault
|
||||||
|
*/
|
||||||
|
type GetTokenDefault struct {
|
||||||
|
_statusCode int
|
||||||
|
|
||||||
|
// In: body
|
||||||
|
Payload *models.Error `json:"body,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGetTokenDefault creates GetTokenDefault with default headers values
|
||||||
|
func NewGetTokenDefault(code int) *GetTokenDefault {
|
||||||
|
if code <= 0 {
|
||||||
|
code = 500
|
||||||
|
}
|
||||||
|
|
||||||
|
return &GetTokenDefault{
|
||||||
|
_statusCode: code,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithStatusCode adds the status to the get token default response
|
||||||
|
func (o *GetTokenDefault) WithStatusCode(code int) *GetTokenDefault {
|
||||||
|
o._statusCode = code
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetStatusCode sets the status to the get token default response
|
||||||
|
func (o *GetTokenDefault) SetStatusCode(code int) {
|
||||||
|
o._statusCode = code
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithPayload adds the payload to the get token default response
|
||||||
|
func (o *GetTokenDefault) WithPayload(payload *models.Error) *GetTokenDefault {
|
||||||
|
o.Payload = payload
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPayload sets the payload to the get token default response
|
||||||
|
func (o *GetTokenDefault) SetPayload(payload *models.Error) {
|
||||||
|
o.Payload = payload
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteResponse to the client
|
||||||
|
func (o *GetTokenDefault) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
|
||||||
|
|
||||||
|
rw.WriteHeader(o._statusCode)
|
||||||
|
if o.Payload != nil {
|
||||||
|
if err := producer.Produce(rw, o.Payload); err != nil {
|
||||||
|
panic(err) // let the recovery middleware deal with this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
34
stores.go
34
stores.go
|
@ -6,35 +6,17 @@ import (
|
||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Permission is a specific allowance for `User` or `Role`.
|
|
||||||
type Permission string
|
|
||||||
type Permissions []Permission
|
|
||||||
|
|
||||||
// UserID is a unique ID for a source user.
|
// UserID is a unique ID for a source user.
|
||||||
type UserID int
|
type UserID int
|
||||||
|
|
||||||
// Represents an authenticated user.
|
// Represents an authenticated user.
|
||||||
type User struct {
|
type User struct {
|
||||||
ID UserID
|
ID UserID
|
||||||
Name string
|
Name string
|
||||||
Permissions Permissions
|
|
||||||
Roles []Role
|
|
||||||
}
|
|
||||||
|
|
||||||
// Role is a set of permissions that may be associated with `User`s
|
|
||||||
type Role struct {
|
|
||||||
ID int
|
|
||||||
Name string
|
|
||||||
Permissions Permissions
|
|
||||||
Users []User
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthStore is the Storage and retrieval of authentication information
|
// AuthStore is the Storage and retrieval of authentication information
|
||||||
type AuthStore struct {
|
type AuthStore struct {
|
||||||
Permissions interface {
|
|
||||||
// Returns a list of all possible permissions support by the AuthStore.
|
|
||||||
All(context.Context) (Permissions, error)
|
|
||||||
}
|
|
||||||
// User management for the AuthStore
|
// User management for the AuthStore
|
||||||
Users interface {
|
Users interface {
|
||||||
// Create a new User in the AuthStore
|
// Create a new User in the AuthStore
|
||||||
|
@ -46,18 +28,6 @@ type AuthStore struct {
|
||||||
// Update the user's permissions or roles
|
// Update the user's permissions or roles
|
||||||
Update(context.Context, User) error
|
Update(context.Context, User) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Roles are sets of permissions.
|
|
||||||
Roles interface {
|
|
||||||
// Create a new role to encapsulate a set of permissions.
|
|
||||||
Add(context.Context, Role) error
|
|
||||||
// Delete the role
|
|
||||||
Delete(context.Context, Role) error
|
|
||||||
// Retrieve the role and the associated users if `ID` exists.
|
|
||||||
Get(ctx context.Context, ID int) error
|
|
||||||
// Update the role to change permissions or users.
|
|
||||||
Update(context.Context, Role) error
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExplorationID is a unique ID for an Exploration.
|
// ExplorationID is a unique ID for an Exploration.
|
||||||
|
|
17
swagger.yaml
17
swagger.yaml
|
@ -1020,6 +1020,20 @@ paths:
|
||||||
description: A processing or an unexpected error.
|
description: A processing or an unexpected error.
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/Error'
|
$ref: '#/definitions/Error'
|
||||||
|
/token:
|
||||||
|
get:
|
||||||
|
summary: Authentication token
|
||||||
|
description: |
|
||||||
|
Generates a JWT authentication token
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: A JWT authentication token
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/Token'
|
||||||
|
default:
|
||||||
|
description: Unexpected internal service error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/Error'
|
||||||
definitions:
|
definitions:
|
||||||
Kapacitors:
|
Kapacitors:
|
||||||
type: object
|
type: object
|
||||||
|
@ -1213,6 +1227,9 @@ definitions:
|
||||||
$ref: "#/definitions/Users"
|
$ref: "#/definitions/Users"
|
||||||
link:
|
link:
|
||||||
$ref: "#/definitions/Link"
|
$ref: "#/definitions/Link"
|
||||||
|
Token:
|
||||||
|
type: object
|
||||||
|
description: a stringified JWT token.
|
||||||
Users:
|
Users:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
package chronograf
|
|
||||||
|
|
||||||
// Transformer will transform the `Response` data.
|
|
||||||
type Transformer interface {
|
|
||||||
Transform(Response) (Response, error)
|
|
||||||
}
|
|
38
uuid/v4.go
38
uuid/v4.go
|
@ -1,9 +1,45 @@
|
||||||
package uuid
|
package uuid
|
||||||
|
|
||||||
import uuid "github.com/satori/go.uuid"
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/influxdata/chronograf"
|
||||||
|
uuid "github.com/satori/go.uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// V4 implements chronograf.ID
|
||||||
type V4 struct{}
|
type V4 struct{}
|
||||||
|
|
||||||
|
// Generate creates a UUID v4 string
|
||||||
func (i *V4) Generate() (string, error) {
|
func (i *V4) Generate() (string, error) {
|
||||||
return uuid.NewV4().String(), nil
|
return uuid.NewV4().String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// APIKey implements chronograf.Authenticator using V4
|
||||||
|
type APIKey struct {
|
||||||
|
Key string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAPIKey creates an APIKey with a UUID v4 Key
|
||||||
|
func NewAPIKey() chronograf.Authenticator {
|
||||||
|
v4 := V4{}
|
||||||
|
key, _ := v4.Generate()
|
||||||
|
return &APIKey{
|
||||||
|
Key: key,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate checks the key against the UUID v4 key
|
||||||
|
func (k *APIKey) Authenticate(ctx context.Context, key string) (chronograf.Principal, error) {
|
||||||
|
if key != k.Key {
|
||||||
|
return "", chronograf.ErrAuthentication
|
||||||
|
}
|
||||||
|
return "admin", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token returns the UUID v4 key
|
||||||
|
func (k *APIKey) Token(context.Context, chronograf.Principal, time.Duration) (string, error) {
|
||||||
|
return k.Key, nil
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
package uuid_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/influxdata/chronograf"
|
||||||
|
"github.com/influxdata/chronograf/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAuthenticate(t *testing.T) {
|
||||||
|
var tests = []struct {
|
||||||
|
Desc string
|
||||||
|
APIKey string
|
||||||
|
Key string
|
||||||
|
Err error
|
||||||
|
User chronograf.Principal
|
||||||
|
}{
|
||||||
|
|
||||||
|
{
|
||||||
|
Desc: "Test auth err when keys are different",
|
||||||
|
APIKey: "key",
|
||||||
|
Key: "badkey",
|
||||||
|
Err: chronograf.ErrAuthentication,
|
||||||
|
User: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Desc: "Test that admin user comes back",
|
||||||
|
APIKey: "key",
|
||||||
|
Key: "key",
|
||||||
|
Err: nil,
|
||||||
|
User: "admin",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
k := uuid.APIKey{
|
||||||
|
Key: test.APIKey,
|
||||||
|
}
|
||||||
|
u, err := k.Authenticate(context.Background(), test.Key)
|
||||||
|
if err != test.Err {
|
||||||
|
t.Errorf("Auth error different; expected %v actual %v", test.Err, err)
|
||||||
|
}
|
||||||
|
if u != test.User {
|
||||||
|
t.Errorf("Auth user different; expected %v actual %v", test.User, u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue