mirror of
https://github.com/kemko/nomad.git
synced 2026-01-05 01:45:44 +03:00
The OIDC provider cache is used by the RPC handler as the OIDC implementation keeps long lived processes running. These process include connections to the remote OIDC provider. The Callback server is used by the CLI and starts when the login command is triggered. This callback server includes success HTML which is displayed when the user successfully logs into the remote OIDC provider.
233 lines
5.9 KiB
Go
233 lines
5.9 KiB
Go
package oidc
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/mitchellh/pointerstructure"
|
|
|
|
"github.com/hashicorp/nomad/nomad/structs"
|
|
)
|
|
|
|
// SelectorData returns the data for go-bexpr for selector evaluation.
|
|
func SelectorData(
|
|
am *structs.ACLAuthMethod, idClaims, userClaims map[string]interface{}) (*structs.ACLAuthClaims, error) {
|
|
|
|
// Ensure the issuer and subscriber data does not get overwritten.
|
|
if len(userClaims) > 0 {
|
|
|
|
iss, issOk := idClaims["iss"]
|
|
sub, subOk := idClaims["sub"]
|
|
|
|
for k, v := range userClaims {
|
|
idClaims[k] = v
|
|
}
|
|
|
|
if issOk {
|
|
idClaims["iss"] = iss
|
|
}
|
|
if subOk {
|
|
idClaims["sub"] = sub
|
|
}
|
|
}
|
|
|
|
return extractClaims(am, idClaims)
|
|
}
|
|
|
|
// extractClaims takes the claim mapping configuration of the OIDC auth method,
|
|
// extracts the claims, and returns a map of data that can be used with
|
|
// go-bexpr.
|
|
func extractClaims(
|
|
am *structs.ACLAuthMethod, all map[string]interface{}) (*structs.ACLAuthClaims, error) {
|
|
|
|
values, err := extractMappings(all, am.Config.ClaimMappings)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
list, err := extractListMappings(all, am.Config.ListClaimMappings)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &structs.ACLAuthClaims{
|
|
Value: values,
|
|
List: list,
|
|
}, nil
|
|
}
|
|
|
|
// extractMappings extracts the string value mappings.
|
|
func extractMappings(
|
|
all map[string]interface{}, mapping map[string]string) (map[string]string, error) {
|
|
|
|
result := make(map[string]string)
|
|
for source, target := range mapping {
|
|
rawValue := getClaim(all, source)
|
|
if rawValue == nil {
|
|
continue
|
|
}
|
|
|
|
strValue, ok := stringifyClaimValue(rawValue)
|
|
if !ok {
|
|
return nil, fmt.Errorf("error converting claim '%s' to string from unknown type %T",
|
|
source, rawValue)
|
|
}
|
|
|
|
result[target] = strValue
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// extractListMappings builds a metadata map of string list values from a set
|
|
// of claims and claims mappings. The referenced claims must be strings and
|
|
// the claims mappings must be of the structure:
|
|
//
|
|
// {
|
|
// "/some/claim/pointer": "metadata_key1",
|
|
// "another_claim": "metadata_key2",
|
|
// ...
|
|
// }
|
|
func extractListMappings(
|
|
all map[string]interface{}, mappings map[string]string) (map[string][]string, error) {
|
|
|
|
result := make(map[string][]string)
|
|
for source, target := range mappings {
|
|
rawValue := getClaim(all, source)
|
|
if rawValue == nil {
|
|
continue
|
|
}
|
|
|
|
rawList, ok := normalizeList(rawValue)
|
|
if !ok {
|
|
return nil, fmt.Errorf("%q list claim could not be converted to string list", source)
|
|
}
|
|
|
|
list := make([]string, 0, len(rawList))
|
|
for _, raw := range rawList {
|
|
value, ok := stringifyClaimValue(raw)
|
|
if !ok {
|
|
return nil, fmt.Errorf("value %v in %q list claim could not be parsed as string",
|
|
raw, source)
|
|
}
|
|
|
|
if value == "" {
|
|
continue
|
|
}
|
|
list = append(list, value)
|
|
}
|
|
|
|
result[target] = list
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// getClaim returns a claim value from allClaims given a provided claim string.
|
|
// If this string is a valid JSONPointer, it will be interpreted as such to
|
|
// locate the claim. Otherwise, the claim string will be used directly.
|
|
//
|
|
// There is no fixup done to the returned data type here. That happens a layer
|
|
// up in the caller.
|
|
func getClaim(all map[string]interface{}, claim string) interface{} {
|
|
if !strings.HasPrefix(claim, "/") {
|
|
return all[claim]
|
|
}
|
|
|
|
val, err := pointerstructure.Get(all, claim)
|
|
if err != nil {
|
|
// We silently drop the error since keys that are invalid
|
|
// just have no values.
|
|
return nil
|
|
}
|
|
|
|
return val
|
|
}
|
|
|
|
// stringifyClaimValue will try to convert the provided raw value into a
|
|
// faithful string representation of that value per these rules:
|
|
//
|
|
// - strings => unchanged
|
|
// - bool => "true" / "false"
|
|
// - json.Number => String()
|
|
// - float32/64 => truncated to int64 and then formatted as an ascii string
|
|
// - intXX/uintXX => casted to int64 and then formatted as an ascii string
|
|
//
|
|
// If successful the string value and true are returned. otherwise an empty
|
|
// string and false are returned.
|
|
func stringifyClaimValue(rawValue interface{}) (string, bool) {
|
|
switch v := rawValue.(type) {
|
|
case string:
|
|
return v, true
|
|
case bool:
|
|
return strconv.FormatBool(v), true
|
|
case json.Number:
|
|
return v.String(), true
|
|
case float64:
|
|
// The claims unmarshalled by go-oidc don't use UseNumber, so
|
|
// they'll come in as float64 instead of an integer or json.Number.
|
|
return strconv.FormatInt(int64(v), 10), true
|
|
|
|
// The numerical type cases following here are only here for the sake
|
|
// of numerical type completion. Everything is truncated to an integer
|
|
// before being stringified.
|
|
case float32:
|
|
return strconv.FormatInt(int64(v), 10), true
|
|
case int8:
|
|
return strconv.FormatInt(int64(v), 10), true
|
|
case int16:
|
|
return strconv.FormatInt(int64(v), 10), true
|
|
case int32:
|
|
return strconv.FormatInt(int64(v), 10), true
|
|
case int64:
|
|
return strconv.FormatInt(v, 10), true
|
|
case int:
|
|
return strconv.FormatInt(int64(v), 10), true
|
|
case uint8:
|
|
return strconv.FormatInt(int64(v), 10), true
|
|
case uint16:
|
|
return strconv.FormatInt(int64(v), 10), true
|
|
case uint32:
|
|
return strconv.FormatInt(int64(v), 10), true
|
|
case uint64:
|
|
return strconv.FormatInt(int64(v), 10), true
|
|
case uint:
|
|
return strconv.FormatInt(int64(v), 10), true
|
|
default:
|
|
return "", false
|
|
}
|
|
}
|
|
|
|
// normalizeList takes an item or a slice and returns a slice. This is useful
|
|
// when providers are expected to return a list (typically of strings) but
|
|
// reduce it to a non-slice type when the list count is 1.
|
|
//
|
|
// There is no fixup done to elements of the returned slice here. That happens
|
|
// a layer up in the caller.
|
|
func normalizeList(raw interface{}) ([]interface{}, bool) {
|
|
switch v := raw.(type) {
|
|
case []interface{}:
|
|
return v, true
|
|
case string, // note: this list should be the same as stringifyClaimValue
|
|
bool,
|
|
json.Number,
|
|
float64,
|
|
float32,
|
|
int8,
|
|
int16,
|
|
int32,
|
|
int64,
|
|
int,
|
|
uint8,
|
|
uint16,
|
|
uint32,
|
|
uint64,
|
|
uint:
|
|
return []interface{}{v}, true
|
|
default:
|
|
return nil, false
|
|
}
|
|
}
|