feat: add ability to authenticate single superadmin user (#5400)
* feat: add ability to authenticate single superadmin user This short-circuits any configured authentication, allowing a user with the correctly signed message to act against the api. * Present an expiring message to be signed/verified * Add chronoctl command to ouput token for use in auth header * Add command to generate and store RSA keypair * Test new superadmin token checkspull/5419/head
parent
9664e080df
commit
26458417c7
|
@ -0,0 +1,56 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
flags "github.com/jessevdk/go-flags"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
parser.AddCommand("gen-keypair",
|
||||||
|
"Generate RSA keypair.",
|
||||||
|
"Generate RSA keypair and write to filesystem.",
|
||||||
|
&genKeyCommand{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type genKeyCommand struct {
|
||||||
|
Out flags.Filename `long:"out" description:"File to save keys to. The public key is stored in a file with the same name with \".pub\" appended." default:"chronograf-rsa"`
|
||||||
|
Bits int `long:"bits" description:"Generate RSA keypair with the specified number of bits." default:"4096"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *genKeyCommand) Execute(args []string) error {
|
||||||
|
_, err := os.Stat(string(t.Out))
|
||||||
|
if err == nil {
|
||||||
|
errExit(errors.New("Specify non-existant file to write to."))
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = os.Stat(string(t.Out) + ".pub")
|
||||||
|
if err == nil {
|
||||||
|
errExit(errors.New("Specify non-existant file.pub to write to."))
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := rsa.GenerateKey(rand.Reader, t.Bits)
|
||||||
|
if err != nil {
|
||||||
|
errExit(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ioutil.WriteFile(string(t.Out), pem.EncodeToMemory(&pem.Block{
|
||||||
|
Type: "RSA PRIVATE KEY",
|
||||||
|
Bytes: x509.MarshalPKCS1PrivateKey(key),
|
||||||
|
}), 0600)
|
||||||
|
|
||||||
|
ioutil.WriteFile(string(t.Out)+".pub", pem.EncodeToMemory(&pem.Block{
|
||||||
|
Type: "PUBLIC KEY",
|
||||||
|
Bytes: x509.MarshalPKCS1PublicKey(key.Public().(*rsa.PublicKey)),
|
||||||
|
}), 0644)
|
||||||
|
|
||||||
|
fmt.Printf("Key pair generated and saved at %s and %s.pub\n", t.Out, t.Out)
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
flags "github.com/jessevdk/go-flags"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
parser.AddCommand("token",
|
||||||
|
"Get current token for superadmin user (if configured)",
|
||||||
|
"Token gets and signs the nonce, providing an expiring token to use in the header: 'Authorization: CHRONOGRAF-SHA256 xxx'",
|
||||||
|
&tokenCommand{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type tokenCommand struct {
|
||||||
|
ChronoURL string `long:"chronograf-url" default:"http://localhost:8888" description:"Chronograf's URL." env:"CHRONOGRAF_URL"`
|
||||||
|
PrivKeyFile flags.Filename `long:"priv-key-file" description:"File location of private key for superadmin token authentication." env:"PRIV_KEY_FILE"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tokenCommand) Execute(args []string) error {
|
||||||
|
key, err := parsePrivKey(string(t.PrivKeyFile))
|
||||||
|
if err != nil {
|
||||||
|
errExit(fmt.Errorf("Failed to parse RSA key: %s", err.Error()))
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, err := getNonceMsg(t.ChronoURL)
|
||||||
|
if err != nil {
|
||||||
|
errExit(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dgst, err := signMsg(msg, key)
|
||||||
|
if err != nil {
|
||||||
|
errExit(fmt.Errorf("Failed to sign: %s", err.Error()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(base64.StdEncoding.EncodeToString(dgst))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePrivKey(privKeyFile string) (*rsa.PrivateKey, error) {
|
||||||
|
if privKeyFile == "" {
|
||||||
|
return nil, errors.New("No private key file specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
pemBytes, err := ioutil.ReadFile(string(privKeyFile))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to read file: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
block, _ := pem.Decode(pemBytes)
|
||||||
|
if block == nil {
|
||||||
|
return nil, errors.New("No PEM formatted key found")
|
||||||
|
} else if block.Type != "RSA PRIVATE KEY" {
|
||||||
|
return nil, fmt.Errorf("Unsupported key type %q", block.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
return x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getNonceMsg(url string) ([]byte, error) {
|
||||||
|
req, err := http.NewRequest("GET", url+"/nonce", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to create request: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to get nonce: %s", err.Error())
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
return ioutil.ReadAll(resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func signMsg(msg []byte, key *rsa.PrivateKey) ([]byte, error) {
|
||||||
|
h := crypto.SHA256.New()
|
||||||
|
h.Write(msg)
|
||||||
|
d := h.Sum(nil)
|
||||||
|
|
||||||
|
return rsa.SignPKCS1v15(rand.Reader, key, crypto.SHA256, d)
|
||||||
|
}
|
2
go.mod
2
go.mod
|
@ -18,7 +18,7 @@ require (
|
||||||
github.com/golang/protobuf v1.3.3 // indirect
|
github.com/golang/protobuf v1.3.3 // indirect
|
||||||
github.com/google/go-cmp v0.3.0
|
github.com/google/go-cmp v0.3.0
|
||||||
github.com/google/go-github v17.0.0+incompatible
|
github.com/google/go-github v17.0.0+incompatible
|
||||||
github.com/google/uuid v1.1.1 // indirect
|
github.com/google/uuid v1.1.1
|
||||||
github.com/goreleaser/goreleaser v0.97.0 // indirect
|
github.com/goreleaser/goreleaser v0.97.0 // indirect
|
||||||
github.com/gorilla/websocket v1.4.1 // indirect
|
github.com/gorilla/websocket v1.4.1 // indirect
|
||||||
github.com/grpc-ecosystem/go-grpc-middleware v1.1.0 // indirect
|
github.com/grpc-ecosystem/go-grpc-middleware v1.1.0 // indirect
|
||||||
|
|
|
@ -2,9 +2,15 @@ package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto"
|
||||||
|
"crypto/rsa"
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/influxdata/chronograf"
|
"github.com/influxdata/chronograf"
|
||||||
"github.com/influxdata/chronograf/oauth2"
|
"github.com/influxdata/chronograf/oauth2"
|
||||||
"github.com/influxdata/chronograf/organizations"
|
"github.com/influxdata/chronograf/organizations"
|
||||||
|
@ -30,6 +36,11 @@ func AuthorizedToken(auth oauth2.Authenticator, logger chronograf.Logger, next h
|
||||||
WithField("method", r.Method).
|
WithField("method", r.Method).
|
||||||
WithField("url", r.URL)
|
WithField("url", r.URL)
|
||||||
|
|
||||||
|
if validSignature(log, r.Header.Get("Authorization")) {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
// We do not check the authorization of the principal. Those
|
// We do not check the authorization of the principal. Those
|
||||||
// served further down the chain should do so.
|
// served further down the chain should do so.
|
||||||
|
@ -83,6 +94,66 @@ func RawStoreAccess(logger chronograf.Logger, next http.HandlerFunc) http.Handle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nonce returns an nonce message to be signed.
|
||||||
|
func nonce(expires time.Duration) func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Add("Expires", msgLastSet.Add(expires).Format(time.RFC1123))
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
|
w.Write([]byte(signerMessage))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
signerMessage = uuid.New().String() // signerMessage is the message to sign with the superadmin user's private key.
|
||||||
|
msgLastSet = time.Now()
|
||||||
|
)
|
||||||
|
|
||||||
|
func rotateSuperAdminNonce(ctx context.Context, expires time.Duration) {
|
||||||
|
tick := time.NewTicker(expires)
|
||||||
|
defer tick.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-tick.C:
|
||||||
|
msgLastSet = time.Now()
|
||||||
|
signerMessage = uuid.New().String()
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validSignature validates the message was signed with the private key corresponding
|
||||||
|
// to the public key given to chronograf on start. Ideally, we would provide the
|
||||||
|
// message to be signed to the user in another call. This would allow old signature/msg
|
||||||
|
// pairs to be "expired".
|
||||||
|
func validSignature(log chronograf.Logger, authHeader string) bool {
|
||||||
|
if publicKey == nil || authHeader == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
sig := strings.TrimSpace(strings.TrimPrefix(authHeader, "CHRONOGRAF-SHA256"))
|
||||||
|
|
||||||
|
h := crypto.SHA256.New()
|
||||||
|
h.Write([]byte(signerMessage))
|
||||||
|
d := h.Sum(nil)
|
||||||
|
|
||||||
|
data, err := base64.StdEncoding.DecodeString(sig)
|
||||||
|
if err != nil {
|
||||||
|
log.Debug("Failed to base64 decode signature")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
err = rsa.VerifyPKCS1v15(publicKey, crypto.SHA256, d, data)
|
||||||
|
if err != nil {
|
||||||
|
log.Debug("Failed to verify signature: ", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// AuthorizedUser extracts the user name and provider from context. If the
|
// AuthorizedUser extracts the user name and provider from context. If the
|
||||||
// user and provider can be found on the context, we look up the user by their
|
// user and provider can be found on the context, we look up the user by their
|
||||||
// name and provider. If the user is found, we verify that the user has at at
|
// name and provider. If the user is found, we verify that the user has at at
|
||||||
|
@ -111,6 +182,18 @@ func AuthorizedUser(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if validSignature(log, r.Header.Get("Authorization")) {
|
||||||
|
// If there is super admin auth, then set the organization id to be the deault org id on context
|
||||||
|
// so that calls like hasOrganizationContext as used in Organization Config service
|
||||||
|
// method OrganizationConfig can successfully get the organization id
|
||||||
|
ctx = context.WithValue(ctx, organizations.ContextKey, defaultOrg.ID)
|
||||||
|
|
||||||
|
// And if there is super admin auth, then give the user raw access to the DataStore
|
||||||
|
r = r.WithContext(serverContext(ctx))
|
||||||
|
next(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if !useAuth {
|
if !useAuth {
|
||||||
// If there is no auth, then set the organization id to be the default org id on context
|
// If there is no auth, then set the organization id to be the default org id on context
|
||||||
// so that calls like hasOrganizationContext as used in Organization Config service
|
// so that calls like hasOrganizationContext as used in Organization Config service
|
||||||
|
|
|
@ -2,6 +2,10 @@ package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -13,6 +17,7 @@ import (
|
||||||
"github.com/influxdata/chronograf/mocks"
|
"github.com/influxdata/chronograf/mocks"
|
||||||
"github.com/influxdata/chronograf/oauth2"
|
"github.com/influxdata/chronograf/oauth2"
|
||||||
"github.com/influxdata/chronograf/roles"
|
"github.com/influxdata/chronograf/roles"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAuthorizedToken(t *testing.T) {
|
func TestAuthorizedToken(t *testing.T) {
|
||||||
|
@ -63,6 +68,7 @@ func TestAuthorizedToken(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAuthorizedUser(t *testing.T) {
|
func TestAuthorizedUser(t *testing.T) {
|
||||||
type fields struct {
|
type fields struct {
|
||||||
UsersStore chronograf.UsersStore
|
UsersStore chronograf.UsersStore
|
||||||
|
@ -70,10 +76,11 @@ func TestAuthorizedUser(t *testing.T) {
|
||||||
Logger chronograf.Logger
|
Logger chronograf.Logger
|
||||||
}
|
}
|
||||||
type args struct {
|
type args struct {
|
||||||
principal *oauth2.Principal
|
principal *oauth2.Principal
|
||||||
scheme string
|
scheme string
|
||||||
useAuth bool
|
useAuth bool
|
||||||
role string
|
role string
|
||||||
|
authHeader string
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
@ -85,6 +92,29 @@ func TestAuthorizedUser(t *testing.T) {
|
||||||
hasServerContext bool
|
hasServerContext bool
|
||||||
authorized bool
|
authorized bool
|
||||||
}{
|
}{
|
||||||
|
{
|
||||||
|
name: "Use superadmin token",
|
||||||
|
fields: fields{
|
||||||
|
UsersStore: &mocks.UsersStore{},
|
||||||
|
OrganizationsStore: &mocks.OrganizationsStore{
|
||||||
|
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||||
|
return &chronograf.Organization{
|
||||||
|
ID: "0",
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Logger: clog.New(clog.DebugLevel),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
useAuth: true,
|
||||||
|
authHeader: genToken(t),
|
||||||
|
},
|
||||||
|
hasOrganizationContext: true,
|
||||||
|
hasSuperAdminContext: false,
|
||||||
|
hasRoleContext: false,
|
||||||
|
hasServerContext: true,
|
||||||
|
authorized: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Not using auth",
|
name: "Not using auth",
|
||||||
fields: fields{
|
fields: fields{
|
||||||
|
@ -1800,6 +1830,9 @@ func TestAuthorizedUser(t *testing.T) {
|
||||||
"http://any.url", // can be any valid URL as we are bypassing mux
|
"http://any.url", // can be any valid URL as we are bypassing mux
|
||||||
nil,
|
nil,
|
||||||
)
|
)
|
||||||
|
if tt.args.authHeader != "" {
|
||||||
|
r.Header.Set("Authorization", tt.args.authHeader)
|
||||||
|
}
|
||||||
if tt.args.principal == nil {
|
if tt.args.principal == nil {
|
||||||
r = r.WithContext(context.WithValue(r.Context(), oauth2.PrincipalKey, nil))
|
r = r.WithContext(context.WithValue(r.Context(), oauth2.PrincipalKey, nil))
|
||||||
} else {
|
} else {
|
||||||
|
@ -1945,7 +1978,28 @@ func TestRawStoreAccess(t *testing.T) {
|
||||||
if hasServerCtx != tt.wants.hasServerContext {
|
if hasServerCtx != tt.wants.hasServerContext {
|
||||||
t.Errorf("%q. RawStoreAccess().Context().Server = %v, expected %v", tt.name, hasServerCtx, tt.wants.hasServerContext)
|
t.Errorf("%q. RawStoreAccess().Context().Server = %v, expected %v", tt.name, hasServerCtx, tt.wants.hasServerContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidSignature(t *testing.T) {
|
||||||
|
require.True(t, validSignature(mocks.NewLogger(), genToken(t)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func genToken(t *testing.T) string {
|
||||||
|
key, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
signerMessage = "abc123"
|
||||||
|
sha256 := crypto.SHA256
|
||||||
|
h := sha256.New()
|
||||||
|
h.Write([]byte(signerMessage))
|
||||||
|
d := h.Sum(nil)
|
||||||
|
|
||||||
|
x, err := rsa.SignPKCS1v15(rand.Reader, key, sha256, d)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
publicKey = key.Public().(*rsa.PublicKey)
|
||||||
|
|
||||||
|
return base64.StdEncoding.EncodeToString(x)
|
||||||
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"path"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
_ "net/http/pprof"
|
_ "net/http/pprof"
|
||||||
|
|
||||||
|
@ -36,6 +37,7 @@ type MuxOpts struct {
|
||||||
CustomLinks []CustomLink // Any custom external links for client's User menu
|
CustomLinks []CustomLink // Any custom external links for client's User menu
|
||||||
PprofEnabled bool // Mount pprof routes for profiling
|
PprofEnabled bool // Mount pprof routes for profiling
|
||||||
DisableGZip bool // Optionally disable gzip.
|
DisableGZip bool // Optionally disable gzip.
|
||||||
|
nonceExpire time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMux attaches all the route handlers; handler returned servers chronograf.
|
// NewMux attaches all the route handlers; handler returned servers chronograf.
|
||||||
|
@ -150,6 +152,9 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
|
||||||
/* Health */
|
/* Health */
|
||||||
router.GET("/ping", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })
|
router.GET("/ping", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })
|
||||||
|
|
||||||
|
/* Auth */
|
||||||
|
router.GET("/nonce", nonce(opts.nonceExpire))
|
||||||
|
|
||||||
/* API */
|
/* API */
|
||||||
// Organizations
|
// Organizations
|
||||||
router.GET("/chronograf/v1/organizations", EnsureAdmin(service.Organizations))
|
router.GET("/chronograf/v1/organizations", EnsureAdmin(service.Organizations))
|
||||||
|
|
|
@ -2,8 +2,13 @@ package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/rsa"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
|
@ -103,6 +108,10 @@ type Server struct {
|
||||||
|
|
||||||
RedirAuth string `long:"redir-auth-login" description:"Automatically redirect login to specified OAuth provider." env:"REDIR_AUTH_LOGIN"`
|
RedirAuth string `long:"redir-auth-login" description:"Automatically redirect login to specified OAuth provider." env:"REDIR_AUTH_LOGIN"`
|
||||||
|
|
||||||
|
PubKey string `long:"pub-key" description:"Public key or superadmin token authentication" env:"PUB_KEY"`
|
||||||
|
PubKeyFile flags.Filename `long:"pub-key-file" description:"File location of public key for superadmin token authentication." env:"PUB_KEY_FILE"`
|
||||||
|
NonceExpiration time.Duration `long:"nonce-expiration" default:"10m" description:"Duration in which a signed nonce is valid. Used for superadmin token authentication." env:"NONCE_EXPIRATION"`
|
||||||
|
|
||||||
StatusFeedURL string `long:"status-feed-url" description:"URL of a JSON Feed to display as a News Feed on the client Status page." default:"https://influxdata.com/feed/json" env:"STATUS_FEED_URL"`
|
StatusFeedURL string `long:"status-feed-url" description:"URL of a JSON Feed to display as a News Feed on the client Status page." default:"https://influxdata.com/feed/json" env:"STATUS_FEED_URL"`
|
||||||
CustomLinks map[string]string `long:"custom-link" description:"Custom link to be added to the client User menu. Multiple links can be added by using multiple of the same flag with different 'name:url' values, or as an environment variable with comma-separated 'name:url' values. E.g. via flags: '--custom-link=InfluxData:https://www.influxdata.com --custom-link=Chronograf:https://github.com/influxdata/chronograf'. E.g. via environment variable: 'export CUSTOM_LINKS=InfluxData:https://www.influxdata.com,Chronograf:https://github.com/influxdata/chronograf'" env:"CUSTOM_LINKS" env-delim:","`
|
CustomLinks map[string]string `long:"custom-link" description:"Custom link to be added to the client User menu. Multiple links can be added by using multiple of the same flag with different 'name:url' values, or as an environment variable with comma-separated 'name:url' values. E.g. via flags: '--custom-link=InfluxData:https://www.influxdata.com --custom-link=Chronograf:https://github.com/influxdata/chronograf'. E.g. via environment variable: 'export CUSTOM_LINKS=InfluxData:https://www.influxdata.com,Chronograf:https://github.com/influxdata/chronograf'" env:"CUSTOM_LINKS" env-delim:","`
|
||||||
TelegrafSystemInterval time.Duration `long:"telegraf-system-interval" default:"1m" description:"Duration used in the GROUP BY time interval for the hosts list" env:"TELEGRAF_SYSTEM_INTERVAL"`
|
TelegrafSystemInterval time.Duration `long:"telegraf-system-interval" default:"1m" description:"Duration used in the GROUP BY time interval for the hosts list" env:"TELEGRAF_SYSTEM_INTERVAL"`
|
||||||
|
@ -335,8 +344,40 @@ func (s *Server) newBuilders(logger chronograf.Logger) builders {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var publicKey *rsa.PublicKey // pubKey is for the simple super admin jwt-esque check.
|
||||||
|
|
||||||
|
// Set the public key preferring from file, if set.
|
||||||
|
func (s *Server) setPubkey() error {
|
||||||
|
pubKey := []byte(s.PubKey)
|
||||||
|
|
||||||
|
if fil := s.PubKeyFile; fil != "" {
|
||||||
|
key, err := ioutil.ReadFile(string(s.PubKeyFile))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pubKey = key
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pubKey) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
block, _ := pem.Decode(pubKey)
|
||||||
|
if block == nil {
|
||||||
|
return errors.New("no key found")
|
||||||
|
} else if block.Type != "PUBLIC KEY" {
|
||||||
|
return fmt.Errorf("unsupported key type %q", block.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
publicKey, err = x509.ParsePKCS1PublicKey(block.Bytes)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Serve starts and runs the chronograf server
|
// Serve starts and runs the chronograf server
|
||||||
func (s *Server) Serve(ctx context.Context) {
|
func (s *Server) Serve(ctx context.Context) {
|
||||||
|
go rotateSuperAdminNonce(ctx, s.NonceExpiration)
|
||||||
|
|
||||||
logger := clog.New(clog.ParseLevel(s.LogLevel))
|
logger := clog.New(clog.ParseLevel(s.LogLevel))
|
||||||
customLinks, err := NewCustomLinks(s.CustomLinks)
|
customLinks, err := NewCustomLinks(s.CustomLinks)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -347,6 +388,12 @@ func (s *Server) Serve(ctx context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = s.setPubkey()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Unable to set public key ", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
var db kv.Store
|
var db kv.Store
|
||||||
if len(s.EtcdEndpoints) == 0 {
|
if len(s.EtcdEndpoints) == 0 {
|
||||||
db, err = bolt.NewClient(ctx,
|
db, err = bolt.NewClient(ctx,
|
||||||
|
@ -412,6 +459,7 @@ func (s *Server) Serve(ctx context.Context) {
|
||||||
CustomLinks: customLinks,
|
CustomLinks: customLinks,
|
||||||
PprofEnabled: s.PprofEnabled,
|
PprofEnabled: s.PprofEnabled,
|
||||||
DisableGZip: s.DisableGZip,
|
DisableGZip: s.DisableGZip,
|
||||||
|
nonceExpire: s.NonceExpiration,
|
||||||
}, service)
|
}, service)
|
||||||
|
|
||||||
// Add chronograf's version header to all requests
|
// Add chronograf's version header to all requests
|
||||||
|
|
Loading…
Reference in New Issue