
Small other improvements included. Change-Id: Ibcf3fc2f5383a4b1faacff814d492d13d2a5a8e5 Signed-off-by: Ruslan Aliev <raliev@mirantis.com>
345 lines
8.1 KiB
Go
345 lines
8.1 KiB
Go
package auth
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"github.com/spf13/viper"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"opendev.org/airship/armada-go/pkg/log"
|
|
)
|
|
|
|
var Log = func(format string, a ...interface{}) {
|
|
log.Printf(format, a...)
|
|
}
|
|
|
|
// Cache provides the interface for cache implementations.
|
|
type Cache interface {
|
|
//Set stores a value with the given ttl
|
|
Set(key string, value interface{}, ttl time.Duration)
|
|
//Get retrieves a value previously stored in the cache.
|
|
//value has to be a pointer to a data structure that matches the type previously given to Set
|
|
//The return value indicates if a value was found
|
|
Get(key string, value interface{}) bool
|
|
}
|
|
|
|
// Auth is the entrypoint for creating the middleware
|
|
type Auth struct {
|
|
//Keystone v3 endpoint url for validating tokens ( e.g https://some.where:5000/v3)
|
|
Endpoint string
|
|
//User-Agent used for all http request by the middleware. Defaults to go-keystone-middleware/1.0
|
|
UserAgent string
|
|
//A cache implementation the middleware should use for caching tokens. By default, no caching is performed.
|
|
TokenCache Cache
|
|
//How long to cache tokens. Defaults to 5 minutes.
|
|
CacheTime time.Duration
|
|
|
|
//http client to use for requests, default to &http.Client{ Timeout: 5 * time.Second }
|
|
Client *http.Client
|
|
}
|
|
|
|
// New returns a new Auth object initialized with default values
|
|
func New(endpoint string) *Auth {
|
|
auth := &Auth{Endpoint: endpoint}
|
|
auth.ensureDefaults()
|
|
return auth
|
|
}
|
|
|
|
// Handler returns a http handler for use in a middleware chain.
|
|
func (a *Auth) Handler(h http.Handler) gin.HandlerFunc {
|
|
a.ensureDefaults()
|
|
return gin.WrapH(&handler{Auth: a, handler: h})
|
|
}
|
|
|
|
// Validate a token.
|
|
// This is useful if you don't want to use the http middleware
|
|
func (a *Auth) Validate(authToken string) (*Token, error) {
|
|
if a.TokenCache != nil {
|
|
var cachedToken Token
|
|
if ok := a.TokenCache.Get(authToken, &cachedToken); ok && cachedToken.Valid() {
|
|
Log("Found valid token in cache")
|
|
return &cachedToken, nil
|
|
}
|
|
}
|
|
|
|
req, err := http.NewRequest("GET", a.Endpoint+"/auth/tokens?nocatalog", nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("X-Auth-Token", authToken)
|
|
req.Header.Set("X-Subject-Token", authToken)
|
|
req.Header.Set("User-Agent", a.UserAgent)
|
|
|
|
r, err := a.Client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer r.Body.Close()
|
|
|
|
if r.StatusCode >= 400 {
|
|
return nil, errors.New(r.Status)
|
|
}
|
|
|
|
var resp authResponse
|
|
if err = json.NewDecoder(r.Body).Decode(&resp); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if e := resp.Error; e != nil {
|
|
return nil, fmt.Errorf("%s : %s", r.Status, e.Message)
|
|
}
|
|
if r.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("%s", r.Status)
|
|
}
|
|
if resp.Token == nil {
|
|
return nil, errors.New("response didn't contain token context")
|
|
}
|
|
if !resp.Token.Valid() {
|
|
return nil, errors.New("returned token is not valid")
|
|
}
|
|
|
|
if a.TokenCache != nil {
|
|
ttl := a.CacheTime
|
|
//The expiry date of the token provides an upper bound on the cache time
|
|
if expiresIn := resp.Token.ExpiresAt.Sub(time.Now()); expiresIn < a.CacheTime {
|
|
ttl = expiresIn
|
|
}
|
|
a.TokenCache.Set(authToken, *resp.Token, ttl)
|
|
}
|
|
|
|
return resp.Token, nil
|
|
}
|
|
|
|
func (a *Auth) ensureDefaults() {
|
|
|
|
if a.UserAgent == "" {
|
|
a.UserAgent = "go-keystone-middleware/1.0"
|
|
}
|
|
|
|
if a.CacheTime == 0 {
|
|
a.CacheTime = 5 * time.Minute
|
|
}
|
|
|
|
if a.Client == nil {
|
|
a.Client = &http.Client{
|
|
Timeout: 5 * time.Second,
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
type handler struct {
|
|
*Auth
|
|
handler http.Handler
|
|
}
|
|
|
|
func (h *handler) ServeHTTP(_ http.ResponseWriter, req *http.Request) {
|
|
filterIncomingHeaders(req)
|
|
req.Header.Set("X-Identity-Status", "Invalid")
|
|
authToken := req.Header.Get("X-Auth-Token")
|
|
if authToken == "" {
|
|
return
|
|
}
|
|
|
|
context, err := h.Auth.Validate(authToken)
|
|
if err != nil {
|
|
Log("Failed to validate token: %v", err)
|
|
return
|
|
}
|
|
|
|
req.Header.Set("X-Identity-Status", "Confirmed")
|
|
for k, v := range context.headers() {
|
|
req.Header.Set(k, v)
|
|
}
|
|
Log("Auth OK Request validated")
|
|
}
|
|
|
|
// Domain holds information about the scope of a token
|
|
type Domain struct {
|
|
ID string
|
|
Name string
|
|
Enabled bool
|
|
}
|
|
|
|
// Project contains information about the scope of a token
|
|
type Project struct {
|
|
ID string
|
|
Name string
|
|
Enabled bool
|
|
Domain Domain
|
|
}
|
|
|
|
// Token describes the scope of a validated token
|
|
type Token struct {
|
|
ExpiresAt time.Time `json:"expires_at"`
|
|
IssuedAt time.Time `json:"issued_at"`
|
|
User struct {
|
|
ID string
|
|
Name string
|
|
Email string
|
|
Enabled bool
|
|
Domain struct {
|
|
ID string
|
|
Name string
|
|
}
|
|
}
|
|
Project *Project
|
|
Domain *Domain
|
|
Roles []struct {
|
|
ID string
|
|
Name string
|
|
}
|
|
}
|
|
|
|
// Valid returns if the token is valid based on the expiration and issue date
|
|
func (t Token) Valid() bool {
|
|
now := time.Now().Unix()
|
|
return t.IssuedAt.Unix() <= now && now < t.ExpiresAt.Unix()
|
|
}
|
|
|
|
type authResponse struct {
|
|
Error *struct {
|
|
Code int
|
|
Message string
|
|
Title string
|
|
}
|
|
Token *Token
|
|
}
|
|
|
|
func (t Token) headers() map[string]string {
|
|
headers := make(map[string]string)
|
|
headers["X-User-Id"] = t.User.ID
|
|
headers["X-User-Name"] = t.User.Name
|
|
headers["X-User-Domain-Id"] = t.User.Domain.ID
|
|
headers["X-User-Domain-Name"] = t.User.Domain.Name
|
|
|
|
if project := t.Project; project != nil {
|
|
headers["X-Project-Name"] = project.Name
|
|
headers["X-Project-Id"] = project.ID
|
|
headers["X-Project-Domain-Name"] = project.Domain.Name
|
|
headers["X-Project-Domain-Id"] = project.Domain.ID
|
|
|
|
}
|
|
|
|
if domain := t.Domain; domain != nil {
|
|
headers["X-Domain-Id"] = domain.ID
|
|
headers["X-Domain-Name"] = domain.Name
|
|
}
|
|
|
|
if roles := t.Roles; roles != nil {
|
|
roleNames := []string{}
|
|
for _, role := range t.Roles {
|
|
roleNames = append(roleNames, role.Name)
|
|
}
|
|
headers["X-Roles"] = strings.Join(roleNames, ",")
|
|
|
|
}
|
|
|
|
return headers
|
|
}
|
|
|
|
func filterIncomingHeaders(req *http.Request) {
|
|
req.Header.Del("X-Identity-Status")
|
|
req.Header.Del("X-Service-Identity-Status")
|
|
|
|
req.Header.Del("X-Domain-Id")
|
|
req.Header.Del("X-Service-Domain-Id")
|
|
|
|
req.Header.Del("X-Domain-Name")
|
|
req.Header.Del("X-Service-Domain-Name")
|
|
|
|
req.Header.Del("X-Project-Id")
|
|
req.Header.Del("X-Service-Project-Id")
|
|
|
|
req.Header.Del("X-Project-Name")
|
|
req.Header.Del("X-Service-Project-Name")
|
|
|
|
req.Header.Del("X-Project-Domain-Id")
|
|
req.Header.Del("X-Service-Project-Domain-Id")
|
|
|
|
req.Header.Del("X-Project-Domain-Name")
|
|
req.Header.Del("X-Service-Project-Domain-Name")
|
|
|
|
req.Header.Del("X-User-Id")
|
|
req.Header.Del("X-Service-User-Id")
|
|
|
|
req.Header.Del("X-User-Name")
|
|
req.Header.Del("X-Service-User-Name")
|
|
|
|
req.Header.Del("X-User-Domain-Id")
|
|
req.Header.Del("X-Service-User-Domain-Id")
|
|
|
|
req.Header.Del("X-User-Domain-Name")
|
|
req.Header.Del("X-Service-User-Domain-Name")
|
|
|
|
req.Header.Del("X-Roles")
|
|
req.Header.Del("X-Service-Roles")
|
|
|
|
req.Header.Del("X-Service-Catalog")
|
|
|
|
//deprecated Headers
|
|
req.Header.Del("X-Tenant-Id")
|
|
req.Header.Del("X-Tenant")
|
|
req.Header.Del("X-User")
|
|
req.Header.Del("X-Role")
|
|
}
|
|
|
|
func Authenticate() (string, error) {
|
|
authUrl := viper.Sub("keystone_authtoken").GetString("auth_url")
|
|
username := viper.Sub("keystone_authtoken").GetString("username")
|
|
password := viper.Sub("keystone_authtoken").GetString("password")
|
|
projectDomainName := viper.Sub("keystone_authtoken").GetString("project_domain_name")
|
|
projectName := viper.Sub("keystone_authtoken").GetString("project_name")
|
|
userDomainName := viper.Sub("keystone_authtoken").GetString("user_domain_name")
|
|
|
|
jsonData := []byte(fmt.Sprintf(`{
|
|
"auth": {
|
|
"identity": {
|
|
"methods": ["password"],
|
|
"password": {
|
|
"user": {
|
|
"name": "%s",
|
|
"domain": { "id": "%s" },
|
|
"password": "%s"
|
|
}
|
|
}
|
|
},
|
|
"scope": {
|
|
"project": {
|
|
"name": "%s",
|
|
"domain": { "id": "%s" }
|
|
}
|
|
}
|
|
}
|
|
}`, username, userDomainName, password, projectName, projectDomainName))
|
|
|
|
req, err := http.NewRequest("POST", authUrl+"/auth/tokens", bytes.NewBuffer(jsonData))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
client := &http.Client{}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != 201 {
|
|
return "", errors.New("http: not authorized")
|
|
}
|
|
|
|
token := resp.Header.Get("X-Subject-Token")
|
|
if token == "" {
|
|
return "", errors.New("http: keystone token is empty")
|
|
}
|
|
|
|
return token, nil
|
|
}
|