1
0
Fork 0
mirror of https://github.com/arangodb/kube-arangodb.git synced 2024-12-15 17:51:03 +00:00
kube-arangodb/pkg/server/auth.go
2018-07-06 12:44:43 +02:00

201 lines
5.6 KiB
Go

//
// DISCLAIMER
//
// Copyright 2018 ArangoDB GmbH, Cologne, Germany
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Copyright holder is ArangoDB GmbH, Cologne, Germany
//
// Author Ewout Prangsma
//
package server
import (
"fmt"
"net/http"
"strings"
"sync"
"time"
"github.com/dchest/uniuri"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
"github.com/rs/zerolog"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
typedv1 "k8s.io/client-go/kubernetes/typed/core/v1"
)
const (
tokenExpirationTime = time.Hour
bearerPrefix = "bearer "
)
type serverAuthentication struct {
log zerolog.Logger
secrets typedv1.SecretInterface
admin struct {
mutex sync.Mutex
username string
password string
}
tokens struct {
mutex sync.Mutex
tokens map[string]*tokenEntry
}
adminSecretName string
allowAnonymous bool
}
type tokenEntry struct {
Token string
ExpiresAt time.Time
}
func (t *tokenEntry) IsExpired() bool {
return t.ExpiresAt.Before(time.Now())
}
// loginRequest is the JSON structure POSTed to `/login`.
type loginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
// loginResponse is the JSON structure returned from `/login`.
type loginResponse struct {
Token string `json:"token"`
}
// newServerAuthentication creates a new server authentication service
// for the given arguments.
func newServerAuthentication(log zerolog.Logger, secrets typedv1.SecretInterface, adminSecretName string, allowAnonymous bool) *serverAuthentication {
auth := &serverAuthentication{
log: log,
secrets: secrets,
adminSecretName: adminSecretName,
allowAnonymous: allowAnonymous,
}
auth.tokens.tokens = make(map[string]*tokenEntry)
return auth
}
// fetchAdminSecret tries to fetch the admin username & password from the configured Secret.
// Returns username, password, error
func (s *serverAuthentication) fetchAdminSecret() (string, string, error) {
if s.adminSecretName == "" {
return "", "", maskAny(fmt.Errorf("No admin secret name specified"))
}
secret, err := s.secrets.Get(s.adminSecretName, metav1.GetOptions{})
if err != nil {
return "", "", maskAny(err)
}
var username, password string
if raw, found := secret.Data[v1.BasicAuthUsernameKey]; !found {
return "", "", maskAny(fmt.Errorf("Secret '%s' contains no '%s' field", s.adminSecretName, v1.BasicAuthUsernameKey))
} else {
username = string(raw)
}
if raw, found := secret.Data[v1.BasicAuthPasswordKey]; !found {
return "", "", maskAny(fmt.Errorf("Secret '%s' contains no '%s' field", s.adminSecretName, v1.BasicAuthPasswordKey))
} else {
password = string(raw)
}
s.admin.mutex.Lock()
defer s.admin.mutex.Unlock()
s.admin.username = username
s.admin.password = password
return username, password, nil
}
// checkLogin compares the given username+password with the admin credentials.
// If needed admin credentials are loaded first.
func (s *serverAuthentication) checkLogin(username, password string) error {
s.admin.mutex.Lock()
expectedUsername := s.admin.username
expectedPassword := s.admin.password
s.admin.mutex.Unlock()
if expectedUsername == "" {
var err error
if expectedUsername, expectedPassword, err = s.fetchAdminSecret(); err != nil {
s.log.Error().Err(err).Msg("Failed to fetch secret")
return maskAny(errors.Wrap(UnauthorizedError, "admin secret cannot be loaded"))
}
}
if expectedUsername != username || expectedPassword != password {
return maskAny(errors.Wrap(UnauthorizedError, "invalid credentials"))
}
return nil
}
// Handle the authentication check
func (s *serverAuthentication) checkAuthentication(c *gin.Context) {
if s.allowAnonymous {
// All ok
return
}
// Fetch authorization token
authHdr := strings.ToLower(c.Request.Header.Get("Authorization"))
if !strings.HasPrefix(authHdr, bearerPrefix) {
sendError(c, maskAny(errors.Wrap(UnauthorizedError, "missing bearer token")))
c.Abort()
return
}
token := strings.TrimSpace(authHdr[len(bearerPrefix):])
// Lookup token
s.tokens.mutex.Lock()
defer s.tokens.mutex.Unlock()
if entry, found := s.tokens.tokens[token]; !found {
s.log.Debug().Str("token", token).Msg("Invalid token")
sendError(c, maskAny(errors.Wrap(UnauthorizedError, "invalid credentials")))
c.Abort()
return
} else if entry.IsExpired() {
s.log.Debug().Str("token", token).Msg("Token expired")
sendError(c, maskAny(errors.Wrap(UnauthorizedError, "credentials expired")))
c.Abort()
return
} else {
// All good, renew expiration
entry.ExpiresAt = time.Now().Add(tokenExpirationTime)
}
}
// Handle a POST /login request
func (s *serverAuthentication) handleLogin(c *gin.Context) {
var req loginRequest
if err := c.BindJSON(&req); err != nil {
sendError(c, err)
return
}
if err := s.checkLogin(req.Username, req.Password); err != nil {
sendError(c, err)
return
}
// Create new token
token := strings.ToLower(uniuri.New())
s.tokens.mutex.Lock()
defer s.tokens.mutex.Unlock()
s.tokens.tokens[token] = &tokenEntry{
Token: token,
ExpiresAt: time.Now().Add(tokenExpirationTime),
}
// Send response
c.JSON(http.StatusOK, loginResponse{
Token: token,
})
}