armada-go/pkg/auth/auth.go
Ruslan Aliev 6a5ac89168 Introduce RBAC support & deckhand manifests fetching
Small other improvements included.

Change-Id: Ibcf3fc2f5383a4b1faacff814d492d13d2a5a8e5
Signed-off-by: Ruslan Aliev <raliev@mirantis.com>
2024-03-05 16:32:31 -06:00

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
}