// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package auth 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 } }