mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-02 16:29:21 +00:00
move client auth plugins
This commit is contained in:
parent
af2c52085d
commit
2f51cc4ce4
@ -1,20 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2015 The Kubernetes Authors.
|
|
||||||
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// package jsonpath is a template engine using jsonpath syntax,
|
|
||||||
// which can be seen at http://goessner.net/articles/JsonPath/.
|
|
||||||
// In addition, it has {range} {end} function to iterate list and slice.
|
|
||||||
package jsonpath // import "k8s.io/kubernetes/pkg/util/jsonpath"
|
|
@ -1,498 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2015 The Kubernetes Authors.
|
|
||||||
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package jsonpath
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"k8s.io/kubernetes/third_party/forked/golang/template"
|
|
||||||
)
|
|
||||||
|
|
||||||
type JSONPath struct {
|
|
||||||
name string
|
|
||||||
parser *Parser
|
|
||||||
stack [][]reflect.Value //push and pop values in different scopes
|
|
||||||
cur []reflect.Value //current scope values
|
|
||||||
beginRange int
|
|
||||||
inRange int
|
|
||||||
endRange int
|
|
||||||
|
|
||||||
allowMissingKeys bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(name string) *JSONPath {
|
|
||||||
return &JSONPath{
|
|
||||||
name: name,
|
|
||||||
beginRange: 0,
|
|
||||||
inRange: 0,
|
|
||||||
endRange: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AllowMissingKeys allows a caller to specify whether they want an error if a field or map key
|
|
||||||
// cannot be located, or simply an empty result. The receiver is returned for chaining.
|
|
||||||
func (j *JSONPath) AllowMissingKeys(allow bool) *JSONPath {
|
|
||||||
j.allowMissingKeys = allow
|
|
||||||
return j
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse parse the given template, return error
|
|
||||||
func (j *JSONPath) Parse(text string) (err error) {
|
|
||||||
j.parser, err = Parse(j.name, text)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute bounds data into template and write the result
|
|
||||||
func (j *JSONPath) Execute(wr io.Writer, data interface{}) error {
|
|
||||||
fullResults, err := j.FindResults(data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for ix := range fullResults {
|
|
||||||
if err := j.PrintResults(wr, fullResults[ix]); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *JSONPath) FindResults(data interface{}) ([][]reflect.Value, error) {
|
|
||||||
if j.parser == nil {
|
|
||||||
return nil, fmt.Errorf("%s is an incomplete jsonpath template", j.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
j.cur = []reflect.Value{reflect.ValueOf(data)}
|
|
||||||
nodes := j.parser.Root.Nodes
|
|
||||||
fullResult := [][]reflect.Value{}
|
|
||||||
for i := 0; i < len(nodes); i++ {
|
|
||||||
node := nodes[i]
|
|
||||||
results, err := j.walk(j.cur, node)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
//encounter an end node, break the current block
|
|
||||||
if j.endRange > 0 && j.endRange <= j.inRange {
|
|
||||||
j.endRange -= 1
|
|
||||||
break
|
|
||||||
}
|
|
||||||
//encounter a range node, start a range loop
|
|
||||||
if j.beginRange > 0 {
|
|
||||||
j.beginRange -= 1
|
|
||||||
j.inRange += 1
|
|
||||||
for k, value := range results {
|
|
||||||
j.parser.Root.Nodes = nodes[i+1:]
|
|
||||||
if k == len(results)-1 {
|
|
||||||
j.inRange -= 1
|
|
||||||
}
|
|
||||||
nextResults, err := j.FindResults(value.Interface())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
fullResult = append(fullResult, nextResults...)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
fullResult = append(fullResult, results)
|
|
||||||
}
|
|
||||||
return fullResult, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PrintResults write the results into writer
|
|
||||||
func (j *JSONPath) PrintResults(wr io.Writer, results []reflect.Value) error {
|
|
||||||
for i, r := range results {
|
|
||||||
text, err := j.evalToText(r)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if i != len(results)-1 {
|
|
||||||
text = append(text, ' ')
|
|
||||||
}
|
|
||||||
if _, err = wr.Write(text); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// walk visits tree rooted at the given node in DFS order
|
|
||||||
func (j *JSONPath) walk(value []reflect.Value, node Node) ([]reflect.Value, error) {
|
|
||||||
switch node := node.(type) {
|
|
||||||
case *ListNode:
|
|
||||||
return j.evalList(value, node)
|
|
||||||
case *TextNode:
|
|
||||||
return []reflect.Value{reflect.ValueOf(node.Text)}, nil
|
|
||||||
case *FieldNode:
|
|
||||||
return j.evalField(value, node)
|
|
||||||
case *ArrayNode:
|
|
||||||
return j.evalArray(value, node)
|
|
||||||
case *FilterNode:
|
|
||||||
return j.evalFilter(value, node)
|
|
||||||
case *IntNode:
|
|
||||||
return j.evalInt(value, node)
|
|
||||||
case *FloatNode:
|
|
||||||
return j.evalFloat(value, node)
|
|
||||||
case *WildcardNode:
|
|
||||||
return j.evalWildcard(value, node)
|
|
||||||
case *RecursiveNode:
|
|
||||||
return j.evalRecursive(value, node)
|
|
||||||
case *UnionNode:
|
|
||||||
return j.evalUnion(value, node)
|
|
||||||
case *IdentifierNode:
|
|
||||||
return j.evalIdentifier(value, node)
|
|
||||||
default:
|
|
||||||
return value, fmt.Errorf("unexpected Node %v", node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// evalInt evaluates IntNode
|
|
||||||
func (j *JSONPath) evalInt(input []reflect.Value, node *IntNode) ([]reflect.Value, error) {
|
|
||||||
result := make([]reflect.Value, len(input))
|
|
||||||
for i := range input {
|
|
||||||
result[i] = reflect.ValueOf(node.Value)
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// evalFloat evaluates FloatNode
|
|
||||||
func (j *JSONPath) evalFloat(input []reflect.Value, node *FloatNode) ([]reflect.Value, error) {
|
|
||||||
result := make([]reflect.Value, len(input))
|
|
||||||
for i := range input {
|
|
||||||
result[i] = reflect.ValueOf(node.Value)
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// evalList evaluates ListNode
|
|
||||||
func (j *JSONPath) evalList(value []reflect.Value, node *ListNode) ([]reflect.Value, error) {
|
|
||||||
var err error
|
|
||||||
curValue := value
|
|
||||||
for _, node := range node.Nodes {
|
|
||||||
curValue, err = j.walk(curValue, node)
|
|
||||||
if err != nil {
|
|
||||||
return curValue, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return curValue, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// evalIdentifier evaluates IdentifierNode
|
|
||||||
func (j *JSONPath) evalIdentifier(input []reflect.Value, node *IdentifierNode) ([]reflect.Value, error) {
|
|
||||||
results := []reflect.Value{}
|
|
||||||
switch node.Name {
|
|
||||||
case "range":
|
|
||||||
j.stack = append(j.stack, j.cur)
|
|
||||||
j.beginRange += 1
|
|
||||||
results = input
|
|
||||||
case "end":
|
|
||||||
if j.endRange < j.inRange { //inside a loop, break the current block
|
|
||||||
j.endRange += 1
|
|
||||||
break
|
|
||||||
}
|
|
||||||
// the loop is about to end, pop value and continue the following execution
|
|
||||||
if len(j.stack) > 0 {
|
|
||||||
j.cur, j.stack = j.stack[len(j.stack)-1], j.stack[:len(j.stack)-1]
|
|
||||||
} else {
|
|
||||||
return results, fmt.Errorf("not in range, nothing to end")
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return input, fmt.Errorf("unrecognized identifier %v", node.Name)
|
|
||||||
}
|
|
||||||
return results, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// evalArray evaluates ArrayNode
|
|
||||||
func (j *JSONPath) evalArray(input []reflect.Value, node *ArrayNode) ([]reflect.Value, error) {
|
|
||||||
result := []reflect.Value{}
|
|
||||||
for _, value := range input {
|
|
||||||
|
|
||||||
value, isNil := template.Indirect(value)
|
|
||||||
if isNil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if value.Kind() != reflect.Array && value.Kind() != reflect.Slice {
|
|
||||||
return input, fmt.Errorf("%v is not array or slice", value.Type())
|
|
||||||
}
|
|
||||||
params := node.Params
|
|
||||||
if !params[0].Known {
|
|
||||||
params[0].Value = 0
|
|
||||||
}
|
|
||||||
if params[0].Value < 0 {
|
|
||||||
params[0].Value += value.Len()
|
|
||||||
}
|
|
||||||
if !params[1].Known {
|
|
||||||
params[1].Value = value.Len()
|
|
||||||
}
|
|
||||||
|
|
||||||
if params[1].Value < 0 {
|
|
||||||
params[1].Value += value.Len()
|
|
||||||
}
|
|
||||||
|
|
||||||
sliceLength := value.Len()
|
|
||||||
if params[1].Value != params[0].Value { // if you're requesting zero elements, allow it through.
|
|
||||||
if params[0].Value >= sliceLength {
|
|
||||||
return input, fmt.Errorf("array index out of bounds: index %d, length %d", params[0].Value, sliceLength)
|
|
||||||
}
|
|
||||||
if params[1].Value > sliceLength {
|
|
||||||
return input, fmt.Errorf("array index out of bounds: index %d, length %d", params[1].Value-1, sliceLength)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !params[2].Known {
|
|
||||||
value = value.Slice(params[0].Value, params[1].Value)
|
|
||||||
} else {
|
|
||||||
value = value.Slice3(params[0].Value, params[1].Value, params[2].Value)
|
|
||||||
}
|
|
||||||
for i := 0; i < value.Len(); i++ {
|
|
||||||
result = append(result, value.Index(i))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// evalUnion evaluates UnionNode
|
|
||||||
func (j *JSONPath) evalUnion(input []reflect.Value, node *UnionNode) ([]reflect.Value, error) {
|
|
||||||
result := []reflect.Value{}
|
|
||||||
for _, listNode := range node.Nodes {
|
|
||||||
temp, err := j.evalList(input, listNode)
|
|
||||||
if err != nil {
|
|
||||||
return input, err
|
|
||||||
}
|
|
||||||
result = append(result, temp...)
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *JSONPath) findFieldInValue(value *reflect.Value, node *FieldNode) (reflect.Value, error) {
|
|
||||||
t := value.Type()
|
|
||||||
var inlineValue *reflect.Value
|
|
||||||
for ix := 0; ix < t.NumField(); ix++ {
|
|
||||||
f := t.Field(ix)
|
|
||||||
jsonTag := f.Tag.Get("json")
|
|
||||||
parts := strings.Split(jsonTag, ",")
|
|
||||||
if len(parts) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if parts[0] == node.Value {
|
|
||||||
return value.Field(ix), nil
|
|
||||||
}
|
|
||||||
if len(parts[0]) == 0 {
|
|
||||||
val := value.Field(ix)
|
|
||||||
inlineValue = &val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if inlineValue != nil {
|
|
||||||
if inlineValue.Kind() == reflect.Struct {
|
|
||||||
// handle 'inline'
|
|
||||||
match, err := j.findFieldInValue(inlineValue, node)
|
|
||||||
if err != nil {
|
|
||||||
return reflect.Value{}, err
|
|
||||||
}
|
|
||||||
if match.IsValid() {
|
|
||||||
return match, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return value.FieldByName(node.Value), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// evalField evaluates field of struct or key of map.
|
|
||||||
func (j *JSONPath) evalField(input []reflect.Value, node *FieldNode) ([]reflect.Value, error) {
|
|
||||||
results := []reflect.Value{}
|
|
||||||
// If there's no input, there's no output
|
|
||||||
if len(input) == 0 {
|
|
||||||
return results, nil
|
|
||||||
}
|
|
||||||
for _, value := range input {
|
|
||||||
var result reflect.Value
|
|
||||||
value, isNil := template.Indirect(value)
|
|
||||||
if isNil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if value.Kind() == reflect.Struct {
|
|
||||||
var err error
|
|
||||||
if result, err = j.findFieldInValue(&value, node); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else if value.Kind() == reflect.Map {
|
|
||||||
mapKeyType := value.Type().Key()
|
|
||||||
nodeValue := reflect.ValueOf(node.Value)
|
|
||||||
// node value type must be convertible to map key type
|
|
||||||
if !nodeValue.Type().ConvertibleTo(mapKeyType) {
|
|
||||||
return results, fmt.Errorf("%s is not convertible to %s", nodeValue, mapKeyType)
|
|
||||||
}
|
|
||||||
result = value.MapIndex(nodeValue.Convert(mapKeyType))
|
|
||||||
}
|
|
||||||
if result.IsValid() {
|
|
||||||
results = append(results, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(results) == 0 {
|
|
||||||
if j.allowMissingKeys {
|
|
||||||
return results, nil
|
|
||||||
}
|
|
||||||
return results, fmt.Errorf("%s is not found", node.Value)
|
|
||||||
}
|
|
||||||
return results, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// evalWildcard extract all contents of the given value
|
|
||||||
func (j *JSONPath) evalWildcard(input []reflect.Value, node *WildcardNode) ([]reflect.Value, error) {
|
|
||||||
results := []reflect.Value{}
|
|
||||||
for _, value := range input {
|
|
||||||
value, isNil := template.Indirect(value)
|
|
||||||
if isNil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
kind := value.Kind()
|
|
||||||
if kind == reflect.Struct {
|
|
||||||
for i := 0; i < value.NumField(); i++ {
|
|
||||||
results = append(results, value.Field(i))
|
|
||||||
}
|
|
||||||
} else if kind == reflect.Map {
|
|
||||||
for _, key := range value.MapKeys() {
|
|
||||||
results = append(results, value.MapIndex(key))
|
|
||||||
}
|
|
||||||
} else if kind == reflect.Array || kind == reflect.Slice || kind == reflect.String {
|
|
||||||
for i := 0; i < value.Len(); i++ {
|
|
||||||
results = append(results, value.Index(i))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return results, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// evalRecursive visit the given value recursively and push all of them to result
|
|
||||||
func (j *JSONPath) evalRecursive(input []reflect.Value, node *RecursiveNode) ([]reflect.Value, error) {
|
|
||||||
result := []reflect.Value{}
|
|
||||||
for _, value := range input {
|
|
||||||
results := []reflect.Value{}
|
|
||||||
value, isNil := template.Indirect(value)
|
|
||||||
if isNil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
kind := value.Kind()
|
|
||||||
if kind == reflect.Struct {
|
|
||||||
for i := 0; i < value.NumField(); i++ {
|
|
||||||
results = append(results, value.Field(i))
|
|
||||||
}
|
|
||||||
} else if kind == reflect.Map {
|
|
||||||
for _, key := range value.MapKeys() {
|
|
||||||
results = append(results, value.MapIndex(key))
|
|
||||||
}
|
|
||||||
} else if kind == reflect.Array || kind == reflect.Slice || kind == reflect.String {
|
|
||||||
for i := 0; i < value.Len(); i++ {
|
|
||||||
results = append(results, value.Index(i))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(results) != 0 {
|
|
||||||
result = append(result, value)
|
|
||||||
output, err := j.evalRecursive(results, node)
|
|
||||||
if err != nil {
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
result = append(result, output...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// evalFilter filter array according to FilterNode
|
|
||||||
func (j *JSONPath) evalFilter(input []reflect.Value, node *FilterNode) ([]reflect.Value, error) {
|
|
||||||
results := []reflect.Value{}
|
|
||||||
for _, value := range input {
|
|
||||||
value, _ = template.Indirect(value)
|
|
||||||
|
|
||||||
if value.Kind() != reflect.Array && value.Kind() != reflect.Slice {
|
|
||||||
return input, fmt.Errorf("%v is not array or slice and cannot be filtered", value)
|
|
||||||
}
|
|
||||||
for i := 0; i < value.Len(); i++ {
|
|
||||||
temp := []reflect.Value{value.Index(i)}
|
|
||||||
lefts, err := j.evalList(temp, node.Left)
|
|
||||||
|
|
||||||
//case exists
|
|
||||||
if node.Operator == "exists" {
|
|
||||||
if len(lefts) > 0 {
|
|
||||||
results = append(results, value.Index(i))
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return input, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var left, right interface{}
|
|
||||||
if len(lefts) != 1 {
|
|
||||||
return input, fmt.Errorf("can only compare one element at a time")
|
|
||||||
}
|
|
||||||
left = lefts[0].Interface()
|
|
||||||
|
|
||||||
rights, err := j.evalList(temp, node.Right)
|
|
||||||
if err != nil {
|
|
||||||
return input, err
|
|
||||||
}
|
|
||||||
if len(rights) != 1 {
|
|
||||||
return input, fmt.Errorf("can only compare one element at a time")
|
|
||||||
}
|
|
||||||
right = rights[0].Interface()
|
|
||||||
|
|
||||||
pass := false
|
|
||||||
switch node.Operator {
|
|
||||||
case "<":
|
|
||||||
pass, err = template.Less(left, right)
|
|
||||||
case ">":
|
|
||||||
pass, err = template.Greater(left, right)
|
|
||||||
case "==":
|
|
||||||
pass, err = template.Equal(left, right)
|
|
||||||
case "!=":
|
|
||||||
pass, err = template.NotEqual(left, right)
|
|
||||||
case "<=":
|
|
||||||
pass, err = template.LessEqual(left, right)
|
|
||||||
case ">=":
|
|
||||||
pass, err = template.GreaterEqual(left, right)
|
|
||||||
default:
|
|
||||||
return results, fmt.Errorf("unrecognized filter operator %s", node.Operator)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return results, err
|
|
||||||
}
|
|
||||||
if pass {
|
|
||||||
results = append(results, value.Index(i))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return results, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// evalToText translates reflect value to corresponding text
|
|
||||||
func (j *JSONPath) evalToText(v reflect.Value) ([]byte, error) {
|
|
||||||
iface, ok := template.PrintableValue(v)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("can't print type %s", v.Type())
|
|
||||||
}
|
|
||||||
var buffer bytes.Buffer
|
|
||||||
fmt.Fprint(&buffer, iface)
|
|
||||||
return buffer.Bytes(), nil
|
|
||||||
}
|
|
@ -1,239 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2015 The Kubernetes Authors.
|
|
||||||
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package jsonpath
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
// NodeType identifies the type of a parse tree node.
|
|
||||||
type NodeType int
|
|
||||||
|
|
||||||
// Type returns itself and provides an easy default implementation
|
|
||||||
func (t NodeType) Type() NodeType {
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t NodeType) String() string {
|
|
||||||
return NodeTypeName[t]
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
NodeText NodeType = iota
|
|
||||||
NodeArray
|
|
||||||
NodeList
|
|
||||||
NodeField
|
|
||||||
NodeIdentifier
|
|
||||||
NodeFilter
|
|
||||||
NodeInt
|
|
||||||
NodeFloat
|
|
||||||
NodeWildcard
|
|
||||||
NodeRecursive
|
|
||||||
NodeUnion
|
|
||||||
)
|
|
||||||
|
|
||||||
var NodeTypeName = map[NodeType]string{
|
|
||||||
NodeText: "NodeText",
|
|
||||||
NodeArray: "NodeArray",
|
|
||||||
NodeList: "NodeList",
|
|
||||||
NodeField: "NodeField",
|
|
||||||
NodeIdentifier: "NodeIdentifier",
|
|
||||||
NodeFilter: "NodeFilter",
|
|
||||||
NodeInt: "NodeInt",
|
|
||||||
NodeFloat: "NodeFloat",
|
|
||||||
NodeWildcard: "NodeWildcard",
|
|
||||||
NodeRecursive: "NodeRecursive",
|
|
||||||
NodeUnion: "NodeUnion",
|
|
||||||
}
|
|
||||||
|
|
||||||
type Node interface {
|
|
||||||
Type() NodeType
|
|
||||||
String() string
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListNode holds a sequence of nodes.
|
|
||||||
type ListNode struct {
|
|
||||||
NodeType
|
|
||||||
Nodes []Node // The element nodes in lexical order.
|
|
||||||
}
|
|
||||||
|
|
||||||
func newList() *ListNode {
|
|
||||||
return &ListNode{NodeType: NodeList}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *ListNode) append(n Node) {
|
|
||||||
l.Nodes = append(l.Nodes, n)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *ListNode) String() string {
|
|
||||||
return fmt.Sprintf("%s", l.Type())
|
|
||||||
}
|
|
||||||
|
|
||||||
// TextNode holds plain text.
|
|
||||||
type TextNode struct {
|
|
||||||
NodeType
|
|
||||||
Text string // The text; may span newlines.
|
|
||||||
}
|
|
||||||
|
|
||||||
func newText(text string) *TextNode {
|
|
||||||
return &TextNode{NodeType: NodeText, Text: text}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TextNode) String() string {
|
|
||||||
return fmt.Sprintf("%s: %s", t.Type(), t.Text)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FieldNode holds field of struct
|
|
||||||
type FieldNode struct {
|
|
||||||
NodeType
|
|
||||||
Value string
|
|
||||||
}
|
|
||||||
|
|
||||||
func newField(value string) *FieldNode {
|
|
||||||
return &FieldNode{NodeType: NodeField, Value: value}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *FieldNode) String() string {
|
|
||||||
return fmt.Sprintf("%s: %s", f.Type(), f.Value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IdentifierNode holds an identifier
|
|
||||||
type IdentifierNode struct {
|
|
||||||
NodeType
|
|
||||||
Name string
|
|
||||||
}
|
|
||||||
|
|
||||||
func newIdentifier(value string) *IdentifierNode {
|
|
||||||
return &IdentifierNode{
|
|
||||||
NodeType: NodeIdentifier,
|
|
||||||
Name: value,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *IdentifierNode) String() string {
|
|
||||||
return fmt.Sprintf("%s: %s", f.Type(), f.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParamsEntry holds param information for ArrayNode
|
|
||||||
type ParamsEntry struct {
|
|
||||||
Value int
|
|
||||||
Known bool //whether the value is known when parse it
|
|
||||||
}
|
|
||||||
|
|
||||||
// ArrayNode holds start, end, step information for array index selection
|
|
||||||
type ArrayNode struct {
|
|
||||||
NodeType
|
|
||||||
Params [3]ParamsEntry //start, end, step
|
|
||||||
}
|
|
||||||
|
|
||||||
func newArray(params [3]ParamsEntry) *ArrayNode {
|
|
||||||
return &ArrayNode{
|
|
||||||
NodeType: NodeArray,
|
|
||||||
Params: params,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *ArrayNode) String() string {
|
|
||||||
return fmt.Sprintf("%s: %v", a.Type(), a.Params)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FilterNode holds operand and operator information for filter
|
|
||||||
type FilterNode struct {
|
|
||||||
NodeType
|
|
||||||
Left *ListNode
|
|
||||||
Right *ListNode
|
|
||||||
Operator string
|
|
||||||
}
|
|
||||||
|
|
||||||
func newFilter(left, right *ListNode, operator string) *FilterNode {
|
|
||||||
return &FilterNode{
|
|
||||||
NodeType: NodeFilter,
|
|
||||||
Left: left,
|
|
||||||
Right: right,
|
|
||||||
Operator: operator,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *FilterNode) String() string {
|
|
||||||
return fmt.Sprintf("%s: %s %s %s", f.Type(), f.Left, f.Operator, f.Right)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IntNode holds integer value
|
|
||||||
type IntNode struct {
|
|
||||||
NodeType
|
|
||||||
Value int
|
|
||||||
}
|
|
||||||
|
|
||||||
func newInt(num int) *IntNode {
|
|
||||||
return &IntNode{NodeType: NodeInt, Value: num}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *IntNode) String() string {
|
|
||||||
return fmt.Sprintf("%s: %d", i.Type(), i.Value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FloatNode holds float value
|
|
||||||
type FloatNode struct {
|
|
||||||
NodeType
|
|
||||||
Value float64
|
|
||||||
}
|
|
||||||
|
|
||||||
func newFloat(num float64) *FloatNode {
|
|
||||||
return &FloatNode{NodeType: NodeFloat, Value: num}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *FloatNode) String() string {
|
|
||||||
return fmt.Sprintf("%s: %f", i.Type(), i.Value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WildcardNode means a wildcard
|
|
||||||
type WildcardNode struct {
|
|
||||||
NodeType
|
|
||||||
}
|
|
||||||
|
|
||||||
func newWildcard() *WildcardNode {
|
|
||||||
return &WildcardNode{NodeType: NodeWildcard}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *WildcardNode) String() string {
|
|
||||||
return fmt.Sprintf("%s", i.Type())
|
|
||||||
}
|
|
||||||
|
|
||||||
// RecursiveNode means a recursive descent operator
|
|
||||||
type RecursiveNode struct {
|
|
||||||
NodeType
|
|
||||||
}
|
|
||||||
|
|
||||||
func newRecursive() *RecursiveNode {
|
|
||||||
return &RecursiveNode{NodeType: NodeRecursive}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RecursiveNode) String() string {
|
|
||||||
return fmt.Sprintf("%s", r.Type())
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnionNode is union of ListNode
|
|
||||||
type UnionNode struct {
|
|
||||||
NodeType
|
|
||||||
Nodes []*ListNode
|
|
||||||
}
|
|
||||||
|
|
||||||
func newUnion(nodes []*ListNode) *UnionNode {
|
|
||||||
return &UnionNode{NodeType: NodeUnion, Nodes: nodes}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *UnionNode) String() string {
|
|
||||||
return fmt.Sprintf("%s", u.Type())
|
|
||||||
}
|
|
@ -1,433 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2015 The Kubernetes Authors.
|
|
||||||
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package jsonpath
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"unicode"
|
|
||||||
"unicode/utf8"
|
|
||||||
)
|
|
||||||
|
|
||||||
const eof = -1
|
|
||||||
|
|
||||||
const (
|
|
||||||
leftDelim = "{"
|
|
||||||
rightDelim = "}"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Parser struct {
|
|
||||||
Name string
|
|
||||||
Root *ListNode
|
|
||||||
input string
|
|
||||||
cur *ListNode
|
|
||||||
pos int
|
|
||||||
start int
|
|
||||||
width int
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse parsed the given text and return a node Parser.
|
|
||||||
// If an error is encountered, parsing stops and an empty
|
|
||||||
// Parser is returned with the error
|
|
||||||
func Parse(name, text string) (*Parser, error) {
|
|
||||||
p := NewParser(name)
|
|
||||||
err := p.Parse(text)
|
|
||||||
if err != nil {
|
|
||||||
p = nil
|
|
||||||
}
|
|
||||||
return p, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewParser(name string) *Parser {
|
|
||||||
return &Parser{
|
|
||||||
Name: name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseAction parsed the expression inside delimiter
|
|
||||||
func parseAction(name, text string) (*Parser, error) {
|
|
||||||
p, err := Parse(name, fmt.Sprintf("%s%s%s", leftDelim, text, rightDelim))
|
|
||||||
// when error happens, p will be nil, so we need to return here
|
|
||||||
if err != nil {
|
|
||||||
return p, err
|
|
||||||
}
|
|
||||||
p.Root = p.Root.Nodes[0].(*ListNode)
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Parser) Parse(text string) error {
|
|
||||||
p.input = text
|
|
||||||
p.Root = newList()
|
|
||||||
p.pos = 0
|
|
||||||
return p.parseText(p.Root)
|
|
||||||
}
|
|
||||||
|
|
||||||
// consumeText return the parsed text since last cosumeText
|
|
||||||
func (p *Parser) consumeText() string {
|
|
||||||
value := p.input[p.start:p.pos]
|
|
||||||
p.start = p.pos
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
// next returns the next rune in the input.
|
|
||||||
func (p *Parser) next() rune {
|
|
||||||
if int(p.pos) >= len(p.input) {
|
|
||||||
p.width = 0
|
|
||||||
return eof
|
|
||||||
}
|
|
||||||
r, w := utf8.DecodeRuneInString(p.input[p.pos:])
|
|
||||||
p.width = w
|
|
||||||
p.pos += p.width
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
// peek returns but does not consume the next rune in the input.
|
|
||||||
func (p *Parser) peek() rune {
|
|
||||||
r := p.next()
|
|
||||||
p.backup()
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
// backup steps back one rune. Can only be called once per call of next.
|
|
||||||
func (p *Parser) backup() {
|
|
||||||
p.pos -= p.width
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Parser) parseText(cur *ListNode) error {
|
|
||||||
for {
|
|
||||||
if strings.HasPrefix(p.input[p.pos:], leftDelim) {
|
|
||||||
if p.pos > p.start {
|
|
||||||
cur.append(newText(p.consumeText()))
|
|
||||||
}
|
|
||||||
return p.parseLeftDelim(cur)
|
|
||||||
}
|
|
||||||
if p.next() == eof {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Correctly reached EOF.
|
|
||||||
if p.pos > p.start {
|
|
||||||
cur.append(newText(p.consumeText()))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseLeftDelim scans the left delimiter, which is known to be present.
|
|
||||||
func (p *Parser) parseLeftDelim(cur *ListNode) error {
|
|
||||||
p.pos += len(leftDelim)
|
|
||||||
p.consumeText()
|
|
||||||
newNode := newList()
|
|
||||||
cur.append(newNode)
|
|
||||||
cur = newNode
|
|
||||||
return p.parseInsideAction(cur)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Parser) parseInsideAction(cur *ListNode) error {
|
|
||||||
prefixMap := map[string]func(*ListNode) error{
|
|
||||||
rightDelim: p.parseRightDelim,
|
|
||||||
"[?(": p.parseFilter,
|
|
||||||
"..": p.parseRecursive,
|
|
||||||
}
|
|
||||||
for prefix, parseFunc := range prefixMap {
|
|
||||||
if strings.HasPrefix(p.input[p.pos:], prefix) {
|
|
||||||
return parseFunc(cur)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch r := p.next(); {
|
|
||||||
case r == eof || isEndOfLine(r):
|
|
||||||
return fmt.Errorf("unclosed action")
|
|
||||||
case r == ' ':
|
|
||||||
p.consumeText()
|
|
||||||
case r == '@' || r == '$': //the current object, just pass it
|
|
||||||
p.consumeText()
|
|
||||||
case r == '[':
|
|
||||||
return p.parseArray(cur)
|
|
||||||
case r == '"':
|
|
||||||
return p.parseQuote(cur)
|
|
||||||
case r == '.':
|
|
||||||
return p.parseField(cur)
|
|
||||||
case r == '+' || r == '-' || unicode.IsDigit(r):
|
|
||||||
p.backup()
|
|
||||||
return p.parseNumber(cur)
|
|
||||||
case isAlphaNumeric(r):
|
|
||||||
p.backup()
|
|
||||||
return p.parseIdentifier(cur)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unrecognized character in action: %#U", r)
|
|
||||||
}
|
|
||||||
return p.parseInsideAction(cur)
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseRightDelim scans the right delimiter, which is known to be present.
|
|
||||||
func (p *Parser) parseRightDelim(cur *ListNode) error {
|
|
||||||
p.pos += len(rightDelim)
|
|
||||||
p.consumeText()
|
|
||||||
cur = p.Root
|
|
||||||
return p.parseText(cur)
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseIdentifier scans build-in keywords, like "range" "end"
|
|
||||||
func (p *Parser) parseIdentifier(cur *ListNode) error {
|
|
||||||
var r rune
|
|
||||||
for {
|
|
||||||
r = p.next()
|
|
||||||
if isTerminator(r) {
|
|
||||||
p.backup()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
value := p.consumeText()
|
|
||||||
cur.append(newIdentifier(value))
|
|
||||||
return p.parseInsideAction(cur)
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseRecursive scans the recursive desent operator ..
|
|
||||||
func (p *Parser) parseRecursive(cur *ListNode) error {
|
|
||||||
p.pos += len("..")
|
|
||||||
p.consumeText()
|
|
||||||
cur.append(newRecursive())
|
|
||||||
if r := p.peek(); isAlphaNumeric(r) {
|
|
||||||
return p.parseField(cur)
|
|
||||||
}
|
|
||||||
return p.parseInsideAction(cur)
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseNumber scans number
|
|
||||||
func (p *Parser) parseNumber(cur *ListNode) error {
|
|
||||||
r := p.peek()
|
|
||||||
if r == '+' || r == '-' {
|
|
||||||
r = p.next()
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
r = p.next()
|
|
||||||
if r != '.' && !unicode.IsDigit(r) {
|
|
||||||
p.backup()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
value := p.consumeText()
|
|
||||||
i, err := strconv.Atoi(value)
|
|
||||||
if err == nil {
|
|
||||||
cur.append(newInt(i))
|
|
||||||
return p.parseInsideAction(cur)
|
|
||||||
}
|
|
||||||
d, err := strconv.ParseFloat(value, 64)
|
|
||||||
if err == nil {
|
|
||||||
cur.append(newFloat(d))
|
|
||||||
return p.parseInsideAction(cur)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("cannot parse number %s", value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseArray scans array index selection
|
|
||||||
func (p *Parser) parseArray(cur *ListNode) error {
|
|
||||||
Loop:
|
|
||||||
for {
|
|
||||||
switch p.next() {
|
|
||||||
case eof, '\n':
|
|
||||||
return fmt.Errorf("unterminated array")
|
|
||||||
case ']':
|
|
||||||
break Loop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
text := p.consumeText()
|
|
||||||
text = string(text[1 : len(text)-1])
|
|
||||||
if text == "*" {
|
|
||||||
text = ":"
|
|
||||||
}
|
|
||||||
|
|
||||||
//union operator
|
|
||||||
strs := strings.Split(text, ",")
|
|
||||||
if len(strs) > 1 {
|
|
||||||
union := []*ListNode{}
|
|
||||||
for _, str := range strs {
|
|
||||||
parser, err := parseAction("union", fmt.Sprintf("[%s]", strings.Trim(str, " ")))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
union = append(union, parser.Root)
|
|
||||||
}
|
|
||||||
cur.append(newUnion(union))
|
|
||||||
return p.parseInsideAction(cur)
|
|
||||||
}
|
|
||||||
|
|
||||||
// dict key
|
|
||||||
reg := regexp.MustCompile(`^'([^']*)'$`)
|
|
||||||
value := reg.FindStringSubmatch(text)
|
|
||||||
if value != nil {
|
|
||||||
parser, err := parseAction("arraydict", fmt.Sprintf(".%s", value[1]))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, node := range parser.Root.Nodes {
|
|
||||||
cur.append(node)
|
|
||||||
}
|
|
||||||
return p.parseInsideAction(cur)
|
|
||||||
}
|
|
||||||
|
|
||||||
//slice operator
|
|
||||||
reg = regexp.MustCompile(`^(-?[\d]*)(:-?[\d]*)?(:[\d]*)?$`)
|
|
||||||
value = reg.FindStringSubmatch(text)
|
|
||||||
if value == nil {
|
|
||||||
return fmt.Errorf("invalid array index %s", text)
|
|
||||||
}
|
|
||||||
value = value[1:]
|
|
||||||
params := [3]ParamsEntry{}
|
|
||||||
for i := 0; i < 3; i++ {
|
|
||||||
if value[i] != "" {
|
|
||||||
if i > 0 {
|
|
||||||
value[i] = value[i][1:]
|
|
||||||
}
|
|
||||||
if i > 0 && value[i] == "" {
|
|
||||||
params[i].Known = false
|
|
||||||
} else {
|
|
||||||
var err error
|
|
||||||
params[i].Known = true
|
|
||||||
params[i].Value, err = strconv.Atoi(value[i])
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("array index %s is not a number", value[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if i == 1 {
|
|
||||||
params[i].Known = true
|
|
||||||
params[i].Value = params[0].Value + 1
|
|
||||||
} else {
|
|
||||||
params[i].Known = false
|
|
||||||
params[i].Value = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cur.append(newArray(params))
|
|
||||||
return p.parseInsideAction(cur)
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseFilter scans filter inside array selection
|
|
||||||
func (p *Parser) parseFilter(cur *ListNode) error {
|
|
||||||
p.pos += len("[?(")
|
|
||||||
p.consumeText()
|
|
||||||
Loop:
|
|
||||||
for {
|
|
||||||
switch p.next() {
|
|
||||||
case eof, '\n':
|
|
||||||
return fmt.Errorf("unterminated filter")
|
|
||||||
case ')':
|
|
||||||
break Loop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if p.next() != ']' {
|
|
||||||
return fmt.Errorf("unclosed array expect ]")
|
|
||||||
}
|
|
||||||
reg := regexp.MustCompile(`^([^!<>=]+)([!<>=]+)(.+?)$`)
|
|
||||||
text := p.consumeText()
|
|
||||||
text = string(text[:len(text)-2])
|
|
||||||
value := reg.FindStringSubmatch(text)
|
|
||||||
if value == nil {
|
|
||||||
parser, err := parseAction("text", text)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
cur.append(newFilter(parser.Root, newList(), "exists"))
|
|
||||||
} else {
|
|
||||||
leftParser, err := parseAction("left", value[1])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
rightParser, err := parseAction("right", value[3])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
cur.append(newFilter(leftParser.Root, rightParser.Root, value[2]))
|
|
||||||
}
|
|
||||||
return p.parseInsideAction(cur)
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseQuote unquotes string inside double quote
|
|
||||||
func (p *Parser) parseQuote(cur *ListNode) error {
|
|
||||||
Loop:
|
|
||||||
for {
|
|
||||||
switch p.next() {
|
|
||||||
case eof, '\n':
|
|
||||||
return fmt.Errorf("unterminated quoted string")
|
|
||||||
case '"':
|
|
||||||
break Loop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
value := p.consumeText()
|
|
||||||
s, err := strconv.Unquote(value)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unquote string %s error %v", value, err)
|
|
||||||
}
|
|
||||||
cur.append(newText(s))
|
|
||||||
return p.parseInsideAction(cur)
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseField scans a field until a terminator
|
|
||||||
func (p *Parser) parseField(cur *ListNode) error {
|
|
||||||
p.consumeText()
|
|
||||||
for p.advance() {
|
|
||||||
}
|
|
||||||
value := p.consumeText()
|
|
||||||
if value == "*" {
|
|
||||||
cur.append(newWildcard())
|
|
||||||
} else {
|
|
||||||
cur.append(newField(strings.Replace(value, "\\", "", -1)))
|
|
||||||
}
|
|
||||||
return p.parseInsideAction(cur)
|
|
||||||
}
|
|
||||||
|
|
||||||
// advance scans until next non-escaped terminator
|
|
||||||
func (p *Parser) advance() bool {
|
|
||||||
r := p.next()
|
|
||||||
if r == '\\' {
|
|
||||||
p.next()
|
|
||||||
} else if isTerminator(r) {
|
|
||||||
p.backup()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// isTerminator reports whether the input is at valid termination character to appear after an identifier.
|
|
||||||
func isTerminator(r rune) bool {
|
|
||||||
if isSpace(r) || isEndOfLine(r) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
switch r {
|
|
||||||
case eof, '.', ',', '[', ']', '$', '@', '{', '}':
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// isSpace reports whether r is a space character.
|
|
||||||
func isSpace(r rune) bool {
|
|
||||||
return r == ' ' || r == '\t'
|
|
||||||
}
|
|
||||||
|
|
||||||
// isEndOfLine reports whether r is an end-of-line character.
|
|
||||||
func isEndOfLine(r rune) bool {
|
|
||||||
return r == '\r' || r == '\n'
|
|
||||||
}
|
|
||||||
|
|
||||||
// isAlphaNumeric reports whether r is an alphabetic, digit, or underscore.
|
|
||||||
func isAlphaNumeric(r rune) bool {
|
|
||||||
return r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r)
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
package(default_visibility = ["//visibility:public"])
|
|
||||||
|
|
||||||
licenses(["notice"])
|
|
||||||
|
|
||||||
load(
|
|
||||||
"@io_bazel_rules_go//go:def.bzl",
|
|
||||||
"go_library",
|
|
||||||
)
|
|
||||||
|
|
||||||
go_library(
|
|
||||||
name = "go_default_library",
|
|
||||||
srcs = ["plugins.go"],
|
|
||||||
tags = ["automanaged"],
|
|
||||||
deps = [
|
|
||||||
"//plugin/pkg/client/auth/gcp:go_default_library",
|
|
||||||
"//plugin/pkg/client/auth/oidc:go_default_library",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
filegroup(
|
|
||||||
name = "package-srcs",
|
|
||||||
srcs = glob(["**"]),
|
|
||||||
tags = ["automanaged"],
|
|
||||||
visibility = ["//visibility:private"],
|
|
||||||
)
|
|
||||||
|
|
||||||
filegroup(
|
|
||||||
name = "all-srcs",
|
|
||||||
srcs = [
|
|
||||||
":package-srcs",
|
|
||||||
"//plugin/pkg/client/auth/gcp:all-srcs",
|
|
||||||
"//plugin/pkg/client/auth/oidc:all-srcs",
|
|
||||||
],
|
|
||||||
tags = ["automanaged"],
|
|
||||||
)
|
|
@ -1,45 +0,0 @@
|
|||||||
package(default_visibility = ["//visibility:public"])
|
|
||||||
|
|
||||||
licenses(["notice"])
|
|
||||||
|
|
||||||
load(
|
|
||||||
"@io_bazel_rules_go//go:def.bzl",
|
|
||||||
"go_library",
|
|
||||||
"go_test",
|
|
||||||
)
|
|
||||||
|
|
||||||
go_library(
|
|
||||||
name = "go_default_library",
|
|
||||||
srcs = ["gcp.go"],
|
|
||||||
tags = ["automanaged"],
|
|
||||||
deps = [
|
|
||||||
"//pkg/util/jsonpath:go_default_library",
|
|
||||||
"//vendor:github.com/golang/glog",
|
|
||||||
"//vendor:golang.org/x/net/context",
|
|
||||||
"//vendor:golang.org/x/oauth2",
|
|
||||||
"//vendor:golang.org/x/oauth2/google",
|
|
||||||
"//vendor:k8s.io/apimachinery/pkg/util/yaml",
|
|
||||||
"//vendor:k8s.io/client-go/rest",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
go_test(
|
|
||||||
name = "go_default_test",
|
|
||||||
srcs = ["gcp_test.go"],
|
|
||||||
library = ":go_default_library",
|
|
||||||
tags = ["automanaged"],
|
|
||||||
deps = ["//vendor:golang.org/x/oauth2"],
|
|
||||||
)
|
|
||||||
|
|
||||||
filegroup(
|
|
||||||
name = "package-srcs",
|
|
||||||
srcs = glob(["**"]),
|
|
||||||
tags = ["automanaged"],
|
|
||||||
visibility = ["//visibility:private"],
|
|
||||||
)
|
|
||||||
|
|
||||||
filegroup(
|
|
||||||
name = "all-srcs",
|
|
||||||
srcs = [":package-srcs"],
|
|
||||||
tags = ["automanaged"],
|
|
||||||
)
|
|
@ -1,3 +0,0 @@
|
|||||||
assignees:
|
|
||||||
- cjcullen
|
|
||||||
- jlowdermilk
|
|
@ -1,274 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2016 The Kubernetes Authors.
|
|
||||||
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package gcp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/golang/glog"
|
|
||||||
"golang.org/x/net/context"
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
"golang.org/x/oauth2/google"
|
|
||||||
"k8s.io/apimachinery/pkg/util/yaml"
|
|
||||||
restclient "k8s.io/client-go/rest"
|
|
||||||
"k8s.io/kubernetes/pkg/util/jsonpath"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
if err := restclient.RegisterAuthProviderPlugin("gcp", newGCPAuthProvider); err != nil {
|
|
||||||
glog.Fatalf("Failed to register gcp auth plugin: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// gcpAuthProvider is an auth provider plugin that uses GCP credentials to provide
|
|
||||||
// tokens for kubectl to authenticate itself to the apiserver. A sample json config
|
|
||||||
// is provided below with all recognized options described.
|
|
||||||
//
|
|
||||||
// {
|
|
||||||
// 'auth-provider': {
|
|
||||||
// # Required
|
|
||||||
// "name": "gcp",
|
|
||||||
//
|
|
||||||
// 'config': {
|
|
||||||
// # Caching options
|
|
||||||
//
|
|
||||||
// # Raw string data representing cached access token.
|
|
||||||
// "access-token": "ya29.CjWdA4GiBPTt",
|
|
||||||
// # RFC3339Nano expiration timestamp for cached access token.
|
|
||||||
// "expiry": "2016-10-31 22:31:9.123",
|
|
||||||
//
|
|
||||||
// # Command execution options
|
|
||||||
// # These options direct the plugin to execute a specified command and parse
|
|
||||||
// # token and expiry time from the output of the command.
|
|
||||||
//
|
|
||||||
// # Command to execute for access token. String is split on whitespace
|
|
||||||
// # with first field treated as the executable, remaining fields as args.
|
|
||||||
// # Command output will be parsed as JSON.
|
|
||||||
// "cmd-path": "/usr/bin/gcloud config config-helper --output=json",
|
|
||||||
//
|
|
||||||
// # JSONPath to the string field that represents the access token in
|
|
||||||
// # command output. If omitted, defaults to "{.access_token}".
|
|
||||||
// "token-key": "{.credential.access_token}",
|
|
||||||
//
|
|
||||||
// # JSONPath to the string field that represents expiration timestamp
|
|
||||||
// # of the access token in the command output. If omitted, defaults to
|
|
||||||
// # "{.token_expiry}"
|
|
||||||
// "expiry-key": ""{.credential.token_expiry}",
|
|
||||||
//
|
|
||||||
// # golang reference time in the format that the expiration timestamp uses.
|
|
||||||
// # If omitted, defaults to time.RFC3339Nano
|
|
||||||
// "time-fmt": "2006-01-02 15:04:05.999999999"
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
type gcpAuthProvider struct {
|
|
||||||
tokenSource oauth2.TokenSource
|
|
||||||
persister restclient.AuthProviderConfigPersister
|
|
||||||
}
|
|
||||||
|
|
||||||
func newGCPAuthProvider(_ string, gcpConfig map[string]string, persister restclient.AuthProviderConfigPersister) (restclient.AuthProvider, error) {
|
|
||||||
cmd, useCmd := gcpConfig["cmd-path"]
|
|
||||||
var ts oauth2.TokenSource
|
|
||||||
var err error
|
|
||||||
if useCmd {
|
|
||||||
ts, err = newCmdTokenSource(cmd, gcpConfig["token-key"], gcpConfig["expiry-key"], gcpConfig["time-fmt"])
|
|
||||||
} else {
|
|
||||||
ts, err = google.DefaultTokenSource(context.Background(), "https://www.googleapis.com/auth/cloud-platform")
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
cts, err := newCachedTokenSource(gcpConfig["access-token"], gcpConfig["expiry"], persister, ts, gcpConfig)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &gcpAuthProvider{cts, persister}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *gcpAuthProvider) WrapTransport(rt http.RoundTripper) http.RoundTripper {
|
|
||||||
return &oauth2.Transport{
|
|
||||||
Source: g.tokenSource,
|
|
||||||
Base: rt,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *gcpAuthProvider) Login() error { return nil }
|
|
||||||
|
|
||||||
type cachedTokenSource struct {
|
|
||||||
lk sync.Mutex
|
|
||||||
source oauth2.TokenSource
|
|
||||||
accessToken string
|
|
||||||
expiry time.Time
|
|
||||||
persister restclient.AuthProviderConfigPersister
|
|
||||||
cache map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
func newCachedTokenSource(accessToken, expiry string, persister restclient.AuthProviderConfigPersister, ts oauth2.TokenSource, cache map[string]string) (*cachedTokenSource, error) {
|
|
||||||
var expiryTime time.Time
|
|
||||||
if parsedTime, err := time.Parse(time.RFC3339Nano, expiry); err == nil {
|
|
||||||
expiryTime = parsedTime
|
|
||||||
}
|
|
||||||
if cache == nil {
|
|
||||||
cache = make(map[string]string)
|
|
||||||
}
|
|
||||||
return &cachedTokenSource{
|
|
||||||
source: ts,
|
|
||||||
accessToken: accessToken,
|
|
||||||
expiry: expiryTime,
|
|
||||||
persister: persister,
|
|
||||||
cache: cache,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *cachedTokenSource) Token() (*oauth2.Token, error) {
|
|
||||||
tok := t.cachedToken()
|
|
||||||
if tok.Valid() && !tok.Expiry.IsZero() {
|
|
||||||
return tok, nil
|
|
||||||
}
|
|
||||||
tok, err := t.source.Token()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
cache := t.update(tok)
|
|
||||||
if t.persister != nil {
|
|
||||||
if err := t.persister.Persist(cache); err != nil {
|
|
||||||
glog.V(4).Infof("Failed to persist token: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tok, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *cachedTokenSource) cachedToken() *oauth2.Token {
|
|
||||||
t.lk.Lock()
|
|
||||||
defer t.lk.Unlock()
|
|
||||||
return &oauth2.Token{
|
|
||||||
AccessToken: t.accessToken,
|
|
||||||
TokenType: "Bearer",
|
|
||||||
Expiry: t.expiry,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *cachedTokenSource) update(tok *oauth2.Token) map[string]string {
|
|
||||||
t.lk.Lock()
|
|
||||||
defer t.lk.Unlock()
|
|
||||||
t.accessToken = tok.AccessToken
|
|
||||||
t.expiry = tok.Expiry
|
|
||||||
ret := map[string]string{}
|
|
||||||
for k, v := range t.cache {
|
|
||||||
ret[k] = v
|
|
||||||
}
|
|
||||||
ret["access-token"] = t.accessToken
|
|
||||||
ret["expiry"] = t.expiry.Format(time.RFC3339Nano)
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
type commandTokenSource struct {
|
|
||||||
cmd string
|
|
||||||
args []string
|
|
||||||
tokenKey string
|
|
||||||
expiryKey string
|
|
||||||
timeFmt string
|
|
||||||
}
|
|
||||||
|
|
||||||
func newCmdTokenSource(cmd, tokenKey, expiryKey, timeFmt string) (*commandTokenSource, error) {
|
|
||||||
if len(timeFmt) == 0 {
|
|
||||||
timeFmt = time.RFC3339Nano
|
|
||||||
}
|
|
||||||
if len(tokenKey) == 0 {
|
|
||||||
tokenKey = "{.access_token}"
|
|
||||||
}
|
|
||||||
if len(expiryKey) == 0 {
|
|
||||||
expiryKey = "{.token_expiry}"
|
|
||||||
}
|
|
||||||
fields := strings.Fields(cmd)
|
|
||||||
if len(fields) == 0 {
|
|
||||||
return nil, fmt.Errorf("missing access token cmd")
|
|
||||||
}
|
|
||||||
return &commandTokenSource{
|
|
||||||
cmd: fields[0],
|
|
||||||
args: fields[1:],
|
|
||||||
tokenKey: tokenKey,
|
|
||||||
expiryKey: expiryKey,
|
|
||||||
timeFmt: timeFmt,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *commandTokenSource) Token() (*oauth2.Token, error) {
|
|
||||||
fullCmd := fmt.Sprintf("%s %s", c.cmd, strings.Join(c.args, " "))
|
|
||||||
cmd := exec.Command(c.cmd, c.args...)
|
|
||||||
output, err := cmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error executing access token command %q: %v", fullCmd, err)
|
|
||||||
}
|
|
||||||
token, err := c.parseTokenCmdOutput(output)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error parsing output for access token command %q: %v", fullCmd, err)
|
|
||||||
}
|
|
||||||
return token, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *commandTokenSource) parseTokenCmdOutput(output []byte) (*oauth2.Token, error) {
|
|
||||||
output, err := yaml.ToJSON(output)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var data interface{}
|
|
||||||
if err := json.Unmarshal(output, &data); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
accessToken, err := parseJSONPath(data, "token-key", c.tokenKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error parsing token-key %q: %v", c.tokenKey, err)
|
|
||||||
}
|
|
||||||
expiryStr, err := parseJSONPath(data, "expiry-key", c.expiryKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error parsing expiry-key %q: %v", c.expiryKey, err)
|
|
||||||
}
|
|
||||||
var expiry time.Time
|
|
||||||
if t, err := time.Parse(c.timeFmt, expiryStr); err != nil {
|
|
||||||
glog.V(4).Infof("Failed to parse token expiry from %s (fmt=%s): %v", expiryStr, c.timeFmt, err)
|
|
||||||
} else {
|
|
||||||
expiry = t
|
|
||||||
}
|
|
||||||
|
|
||||||
return &oauth2.Token{
|
|
||||||
AccessToken: accessToken,
|
|
||||||
TokenType: "Bearer",
|
|
||||||
Expiry: expiry,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseJSONPath(input interface{}, name, template string) (string, error) {
|
|
||||||
j := jsonpath.New(name)
|
|
||||||
buf := new(bytes.Buffer)
|
|
||||||
if err := j.Parse(template); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if err := j.Execute(buf, input); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return buf.String(), nil
|
|
||||||
}
|
|
@ -1,211 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2016 The Kubernetes Authors.
|
|
||||||
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package gcp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCmdTokenSource(t *testing.T) {
|
|
||||||
fakeExpiry := time.Date(2016, 10, 31, 22, 31, 9, 123000000, time.UTC)
|
|
||||||
customFmt := "2006-01-02 15:04:05.999999999"
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
output []byte
|
|
||||||
cmd, tokenKey, expiryKey, timeFmt string
|
|
||||||
tok *oauth2.Token
|
|
||||||
expectErr error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
"defaults",
|
|
||||||
[]byte(`{
|
|
||||||
"access_token": "faketoken",
|
|
||||||
"token_expiry": "2016-10-31T22:31:09.123000000Z"
|
|
||||||
}`),
|
|
||||||
"/fake/cmd/path", "", "", "",
|
|
||||||
&oauth2.Token{
|
|
||||||
AccessToken: "faketoken",
|
|
||||||
TokenType: "Bearer",
|
|
||||||
Expiry: fakeExpiry,
|
|
||||||
},
|
|
||||||
nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"custom keys",
|
|
||||||
[]byte(`{
|
|
||||||
"token": "faketoken",
|
|
||||||
"token_expiry": {
|
|
||||||
"datetime": "2016-10-31 22:31:09.123"
|
|
||||||
}
|
|
||||||
}`),
|
|
||||||
"/fake/cmd/path", "{.token}", "{.token_expiry.datetime}", customFmt,
|
|
||||||
&oauth2.Token{
|
|
||||||
AccessToken: "faketoken",
|
|
||||||
TokenType: "Bearer",
|
|
||||||
Expiry: fakeExpiry,
|
|
||||||
},
|
|
||||||
nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"missing cmd",
|
|
||||||
nil,
|
|
||||||
"", "", "", "",
|
|
||||||
nil,
|
|
||||||
fmt.Errorf("missing access token cmd"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"missing token-key",
|
|
||||||
[]byte(`{
|
|
||||||
"broken": "faketoken",
|
|
||||||
"token_expiry": {
|
|
||||||
"datetime": "2016-10-31 22:31:09.123000000Z"
|
|
||||||
}
|
|
||||||
}`),
|
|
||||||
"/fake/cmd/path", "{.token}", "", "",
|
|
||||||
nil,
|
|
||||||
fmt.Errorf("error parsing token-key %q", "{.token}"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"missing expiry-key",
|
|
||||||
[]byte(`{
|
|
||||||
"access_token": "faketoken",
|
|
||||||
"expires": "2016-10-31T22:31:09.123000000Z"
|
|
||||||
}`),
|
|
||||||
"/fake/cmd/path", "", "{.expiry}", "",
|
|
||||||
nil,
|
|
||||||
fmt.Errorf("error parsing expiry-key %q", "{.expiry}"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"invalid expiry timestamp",
|
|
||||||
[]byte(`{
|
|
||||||
"access_token": "faketoken",
|
|
||||||
"token_expiry": "sometime soon, idk"
|
|
||||||
}`),
|
|
||||||
"/fake/cmd/path", "", "", "",
|
|
||||||
&oauth2.Token{
|
|
||||||
AccessToken: "faketoken",
|
|
||||||
TokenType: "Bearer",
|
|
||||||
Expiry: time.Time{},
|
|
||||||
},
|
|
||||||
nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"bad JSON",
|
|
||||||
[]byte(`{
|
|
||||||
"access_token": "faketoken",
|
|
||||||
"token_expiry": "sometime soon, idk"
|
|
||||||
------
|
|
||||||
`),
|
|
||||||
"/fake/cmd", "", "", "",
|
|
||||||
nil,
|
|
||||||
fmt.Errorf("invalid character '-' after object key:value pair"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tests {
|
|
||||||
ts, err := newCmdTokenSource(tc.cmd, tc.tokenKey, tc.expiryKey, tc.timeFmt)
|
|
||||||
if err != nil {
|
|
||||||
if !strings.Contains(err.Error(), tc.expectErr.Error()) {
|
|
||||||
t.Errorf("%s newCmdTokenSource error: %v, want %v", tc.name, err, tc.expectErr)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
tok, err := ts.parseTokenCmdOutput(tc.output)
|
|
||||||
|
|
||||||
if err != tc.expectErr && !strings.Contains(err.Error(), tc.expectErr.Error()) {
|
|
||||||
t.Errorf("%s parseCmdTokenSource error: %v, want %v", tc.name, err, tc.expectErr)
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(tok, tc.tok) {
|
|
||||||
t.Errorf("%s got token %v, want %v", tc.name, tok, tc.tok)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type fakePersister struct {
|
|
||||||
lk sync.Mutex
|
|
||||||
cache map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakePersister) Persist(cache map[string]string) error {
|
|
||||||
f.lk.Lock()
|
|
||||||
defer f.lk.Unlock()
|
|
||||||
f.cache = map[string]string{}
|
|
||||||
for k, v := range cache {
|
|
||||||
f.cache[k] = v
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakePersister) read() map[string]string {
|
|
||||||
ret := map[string]string{}
|
|
||||||
f.lk.Lock()
|
|
||||||
for k, v := range f.cache {
|
|
||||||
ret[k] = v
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
type fakeTokenSource struct {
|
|
||||||
token *oauth2.Token
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeTokenSource) Token() (*oauth2.Token, error) {
|
|
||||||
return f.token, f.err
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCachedTokenSource(t *testing.T) {
|
|
||||||
tok := &oauth2.Token{AccessToken: "fakeaccesstoken"}
|
|
||||||
persister := &fakePersister{}
|
|
||||||
source := &fakeTokenSource{
|
|
||||||
token: tok,
|
|
||||||
err: nil,
|
|
||||||
}
|
|
||||||
cache := map[string]string{
|
|
||||||
"foo": "bar",
|
|
||||||
"baz": "bazinga",
|
|
||||||
}
|
|
||||||
ts, err := newCachedTokenSource("fakeaccesstoken", "", persister, source, cache)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
wg.Add(10)
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
go func() {
|
|
||||||
_, err := ts.Token()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("unexpected error: %s", err)
|
|
||||||
}
|
|
||||||
wg.Done()
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
cache["access-token"] = "fakeaccesstoken"
|
|
||||||
cache["expiry"] = tok.Expiry.Format(time.RFC3339Nano)
|
|
||||||
if got := persister.read(); !reflect.DeepEqual(got, cache) {
|
|
||||||
t.Errorf("got cache %v, want %v", got, cache)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,48 +0,0 @@
|
|||||||
package(default_visibility = ["//visibility:public"])
|
|
||||||
|
|
||||||
licenses(["notice"])
|
|
||||||
|
|
||||||
load(
|
|
||||||
"@io_bazel_rules_go//go:def.bzl",
|
|
||||||
"go_library",
|
|
||||||
"go_test",
|
|
||||||
)
|
|
||||||
|
|
||||||
go_library(
|
|
||||||
name = "go_default_library",
|
|
||||||
srcs = ["oidc.go"],
|
|
||||||
tags = ["automanaged"],
|
|
||||||
deps = [
|
|
||||||
"//vendor:github.com/coreos/go-oidc/jose",
|
|
||||||
"//vendor:github.com/coreos/go-oidc/oauth2",
|
|
||||||
"//vendor:github.com/coreos/go-oidc/oidc",
|
|
||||||
"//vendor:github.com/golang/glog",
|
|
||||||
"//vendor:k8s.io/client-go/rest",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
go_test(
|
|
||||||
name = "go_default_test",
|
|
||||||
srcs = ["oidc_test.go"],
|
|
||||||
library = ":go_default_library",
|
|
||||||
tags = ["automanaged"],
|
|
||||||
deps = [
|
|
||||||
"//plugin/pkg/auth/authenticator/token/oidc/testing:go_default_library",
|
|
||||||
"//vendor:github.com/coreos/go-oidc/jose",
|
|
||||||
"//vendor:github.com/coreos/go-oidc/key",
|
|
||||||
"//vendor:github.com/coreos/go-oidc/oauth2",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
filegroup(
|
|
||||||
name = "package-srcs",
|
|
||||||
srcs = glob(["**"]),
|
|
||||||
tags = ["automanaged"],
|
|
||||||
visibility = ["//visibility:private"],
|
|
||||||
)
|
|
||||||
|
|
||||||
filegroup(
|
|
||||||
name = "all-srcs",
|
|
||||||
srcs = [":package-srcs"],
|
|
||||||
tags = ["automanaged"],
|
|
||||||
)
|
|
@ -1,2 +0,0 @@
|
|||||||
assignees:
|
|
||||||
- ericchiang
|
|
@ -1,333 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2016 The Kubernetes Authors.
|
|
||||||
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package oidc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/coreos/go-oidc/jose"
|
|
||||||
"github.com/coreos/go-oidc/oauth2"
|
|
||||||
"github.com/coreos/go-oidc/oidc"
|
|
||||||
"github.com/golang/glog"
|
|
||||||
|
|
||||||
restclient "k8s.io/client-go/rest"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
cfgIssuerUrl = "idp-issuer-url"
|
|
||||||
cfgClientID = "client-id"
|
|
||||||
cfgClientSecret = "client-secret"
|
|
||||||
cfgCertificateAuthority = "idp-certificate-authority"
|
|
||||||
cfgCertificateAuthorityData = "idp-certificate-authority-data"
|
|
||||||
cfgExtraScopes = "extra-scopes"
|
|
||||||
cfgIDToken = "id-token"
|
|
||||||
cfgRefreshToken = "refresh-token"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
if err := restclient.RegisterAuthProviderPlugin("oidc", newOIDCAuthProvider); err != nil {
|
|
||||||
glog.Fatalf("Failed to register oidc auth plugin: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// expiryDelta determines how earlier a token should be considered
|
|
||||||
// expired than its actual expiration time. It is used to avoid late
|
|
||||||
// expirations due to client-server time mismatches.
|
|
||||||
//
|
|
||||||
// NOTE(ericchiang): this is take from golang.org/x/oauth2
|
|
||||||
const expiryDelta = 10 * time.Second
|
|
||||||
|
|
||||||
var cache = newClientCache()
|
|
||||||
|
|
||||||
// Like TLS transports, keep a cache of OIDC clients indexed by issuer URL.
|
|
||||||
type clientCache struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
cache map[cacheKey]*oidcAuthProvider
|
|
||||||
}
|
|
||||||
|
|
||||||
func newClientCache() *clientCache {
|
|
||||||
return &clientCache{cache: make(map[cacheKey]*oidcAuthProvider)}
|
|
||||||
}
|
|
||||||
|
|
||||||
type cacheKey struct {
|
|
||||||
// Canonical issuer URL string of the provider.
|
|
||||||
issuerURL string
|
|
||||||
|
|
||||||
clientID string
|
|
||||||
clientSecret string
|
|
||||||
|
|
||||||
// Don't use CA as cache key because we only add a cache entry if we can connect
|
|
||||||
// to the issuer in the first place. A valid CA is a prerequisite.
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *clientCache) getClient(issuer, clientID, clientSecret string) (*oidcAuthProvider, bool) {
|
|
||||||
c.mu.RLock()
|
|
||||||
defer c.mu.RUnlock()
|
|
||||||
client, ok := c.cache[cacheKey{issuer, clientID, clientSecret}]
|
|
||||||
return client, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// setClient attempts to put the client in the cache but may return any clients
|
|
||||||
// with the same keys set before. This is so there's only ever one client for a provider.
|
|
||||||
func (c *clientCache) setClient(issuer, clientID, clientSecret string, client *oidcAuthProvider) *oidcAuthProvider {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
key := cacheKey{issuer, clientID, clientSecret}
|
|
||||||
|
|
||||||
// If another client has already initialized a client for the given provider we want
|
|
||||||
// to use that client instead of the one we're trying to set. This is so all transports
|
|
||||||
// share a client and can coordinate around the same mutex when refreshing and writing
|
|
||||||
// to the kubeconfig.
|
|
||||||
if oldClient, ok := c.cache[key]; ok {
|
|
||||||
return oldClient
|
|
||||||
}
|
|
||||||
|
|
||||||
c.cache[key] = client
|
|
||||||
return client
|
|
||||||
}
|
|
||||||
|
|
||||||
func newOIDCAuthProvider(_ string, cfg map[string]string, persister restclient.AuthProviderConfigPersister) (restclient.AuthProvider, error) {
|
|
||||||
issuer := cfg[cfgIssuerUrl]
|
|
||||||
if issuer == "" {
|
|
||||||
return nil, fmt.Errorf("Must provide %s", cfgIssuerUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
clientID := cfg[cfgClientID]
|
|
||||||
if clientID == "" {
|
|
||||||
return nil, fmt.Errorf("Must provide %s", cfgClientID)
|
|
||||||
}
|
|
||||||
|
|
||||||
clientSecret := cfg[cfgClientSecret]
|
|
||||||
if clientSecret == "" {
|
|
||||||
return nil, fmt.Errorf("Must provide %s", cfgClientSecret)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check cache for existing provider.
|
|
||||||
if provider, ok := cache.getClient(issuer, clientID, clientSecret); ok {
|
|
||||||
return provider, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var certAuthData []byte
|
|
||||||
var err error
|
|
||||||
if cfg[cfgCertificateAuthorityData] != "" {
|
|
||||||
certAuthData, err = base64.StdEncoding.DecodeString(cfg[cfgCertificateAuthorityData])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clientConfig := restclient.Config{
|
|
||||||
TLSClientConfig: restclient.TLSClientConfig{
|
|
||||||
CAFile: cfg[cfgCertificateAuthority],
|
|
||||||
CAData: certAuthData,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
trans, err := restclient.TransportFor(&clientConfig)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
hc := &http.Client{Transport: trans}
|
|
||||||
|
|
||||||
providerCfg, err := oidc.FetchProviderConfig(hc, issuer)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error fetching provider config: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
scopes := strings.Split(cfg[cfgExtraScopes], ",")
|
|
||||||
oidcCfg := oidc.ClientConfig{
|
|
||||||
HTTPClient: hc,
|
|
||||||
Credentials: oidc.ClientCredentials{
|
|
||||||
ID: clientID,
|
|
||||||
Secret: clientSecret,
|
|
||||||
},
|
|
||||||
ProviderConfig: providerCfg,
|
|
||||||
Scope: append(scopes, oidc.DefaultScope...),
|
|
||||||
}
|
|
||||||
client, err := oidc.NewClient(oidcCfg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error creating OIDC Client: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
provider := &oidcAuthProvider{
|
|
||||||
client: &oidcClient{client},
|
|
||||||
cfg: cfg,
|
|
||||||
persister: persister,
|
|
||||||
now: time.Now,
|
|
||||||
}
|
|
||||||
|
|
||||||
return cache.setClient(issuer, clientID, clientSecret, provider), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type oidcAuthProvider struct {
|
|
||||||
// Interface rather than a raw *oidc.Client for testing.
|
|
||||||
client OIDCClient
|
|
||||||
|
|
||||||
// Stubbed out for testing.
|
|
||||||
now func() time.Time
|
|
||||||
|
|
||||||
// Mutex guards persisting to the kubeconfig file and allows synchronized
|
|
||||||
// updates to the in-memory config. It also ensures concurrent calls to
|
|
||||||
// the RoundTripper only trigger a single refresh request.
|
|
||||||
mu sync.Mutex
|
|
||||||
cfg map[string]string
|
|
||||||
persister restclient.AuthProviderConfigPersister
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *oidcAuthProvider) WrapTransport(rt http.RoundTripper) http.RoundTripper {
|
|
||||||
return &roundTripper{
|
|
||||||
wrapped: rt,
|
|
||||||
provider: p,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *oidcAuthProvider) Login() error {
|
|
||||||
return errors.New("not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
type OIDCClient interface {
|
|
||||||
refreshToken(rt string) (oauth2.TokenResponse, error)
|
|
||||||
verifyJWT(jwt *jose.JWT) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type roundTripper struct {
|
|
||||||
provider *oidcAuthProvider
|
|
||||||
wrapped http.RoundTripper
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
||||||
token, err := r.provider.idToken()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// shallow copy of the struct
|
|
||||||
r2 := new(http.Request)
|
|
||||||
*r2 = *req
|
|
||||||
// deep copy of the Header so we don't modify the original
|
|
||||||
// request's Header (as per RoundTripper contract).
|
|
||||||
r2.Header = make(http.Header)
|
|
||||||
for k, s := range req.Header {
|
|
||||||
r2.Header[k] = s
|
|
||||||
}
|
|
||||||
r2.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
|
||||||
|
|
||||||
return r.wrapped.RoundTrip(r2)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *oidcAuthProvider) idToken() (string, error) {
|
|
||||||
p.mu.Lock()
|
|
||||||
defer p.mu.Unlock()
|
|
||||||
|
|
||||||
if idToken, ok := p.cfg[cfgIDToken]; ok && len(idToken) > 0 {
|
|
||||||
valid, err := verifyJWTExpiry(p.now(), idToken)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if valid {
|
|
||||||
// If the cached id token is still valid use it.
|
|
||||||
return idToken, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to request a new token using the refresh token.
|
|
||||||
rt, ok := p.cfg[cfgRefreshToken]
|
|
||||||
if !ok || len(rt) == 0 {
|
|
||||||
return "", errors.New("No valid id-token, and cannot refresh without refresh-token")
|
|
||||||
}
|
|
||||||
|
|
||||||
tokens, err := p.client.refreshToken(rt)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("could not refresh token: %v", err)
|
|
||||||
}
|
|
||||||
jwt, err := jose.ParseJWT(tokens.IDToken)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := p.client.verifyJWT(&jwt); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new config to persist.
|
|
||||||
newCfg := make(map[string]string)
|
|
||||||
for key, val := range p.cfg {
|
|
||||||
newCfg[key] = val
|
|
||||||
}
|
|
||||||
|
|
||||||
if tokens.RefreshToken != "" && tokens.RefreshToken != rt {
|
|
||||||
newCfg[cfgRefreshToken] = tokens.RefreshToken
|
|
||||||
}
|
|
||||||
|
|
||||||
newCfg[cfgIDToken] = tokens.IDToken
|
|
||||||
if err = p.persister.Persist(newCfg); err != nil {
|
|
||||||
return "", fmt.Errorf("could not perist new tokens: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the in memory config to reflect the on disk one.
|
|
||||||
p.cfg = newCfg
|
|
||||||
|
|
||||||
return tokens.IDToken, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// oidcClient is the real implementation of the OIDCClient interface, which is
|
|
||||||
// used for testing.
|
|
||||||
type oidcClient struct {
|
|
||||||
client *oidc.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *oidcClient) refreshToken(rt string) (oauth2.TokenResponse, error) {
|
|
||||||
oac, err := o.client.OAuthClient()
|
|
||||||
if err != nil {
|
|
||||||
return oauth2.TokenResponse{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return oac.RequestToken(oauth2.GrantTypeRefreshToken, rt)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *oidcClient) verifyJWT(jwt *jose.JWT) error {
|
|
||||||
return o.client.VerifyJWT(*jwt)
|
|
||||||
}
|
|
||||||
|
|
||||||
func verifyJWTExpiry(now time.Time, s string) (valid bool, err error) {
|
|
||||||
jwt, err := jose.ParseJWT(s)
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("invalid %q", cfgIDToken)
|
|
||||||
}
|
|
||||||
claims, err := jwt.Claims()
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
exp, ok, err := claims.TimeClaim("exp")
|
|
||||||
switch {
|
|
||||||
case err != nil:
|
|
||||||
return false, fmt.Errorf("failed to parse 'exp' claim: %v", err)
|
|
||||||
case !ok:
|
|
||||||
return false, errors.New("missing required 'exp' claim")
|
|
||||||
case exp.After(now.Add(expiryDelta)):
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, nil
|
|
||||||
}
|
|
@ -1,384 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2016 The Kubernetes Authors.
|
|
||||||
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package oidc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/coreos/go-oidc/jose"
|
|
||||||
"github.com/coreos/go-oidc/key"
|
|
||||||
"github.com/coreos/go-oidc/oauth2"
|
|
||||||
|
|
||||||
oidctesting "k8s.io/kubernetes/plugin/pkg/auth/authenticator/token/oidc/testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func clearCache() {
|
|
||||||
cache = newClientCache()
|
|
||||||
}
|
|
||||||
|
|
||||||
type persister struct{}
|
|
||||||
|
|
||||||
// we don't need to actually persist anything because there's no way for us to
|
|
||||||
// read from a persister.
|
|
||||||
func (p *persister) Persist(map[string]string) error { return nil }
|
|
||||||
|
|
||||||
type noRefreshOIDCClient struct{}
|
|
||||||
|
|
||||||
func (c *noRefreshOIDCClient) refreshToken(rt string) (oauth2.TokenResponse, error) {
|
|
||||||
return oauth2.TokenResponse{}, errors.New("alwaysErrOIDCClient: cannot refresh token")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *noRefreshOIDCClient) verifyJWT(jwt *jose.JWT) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type mockOIDCClient struct {
|
|
||||||
tokenResponse oauth2.TokenResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *mockOIDCClient) refreshToken(rt string) (oauth2.TokenResponse, error) {
|
|
||||||
return c.tokenResponse, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *mockOIDCClient) verifyJWT(jwt *jose.JWT) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewOIDCAuthProvider(t *testing.T) {
|
|
||||||
tempDir, err := ioutil.TempDir(os.TempDir(), "oidc_test")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Cannot make temp dir %v", err)
|
|
||||||
}
|
|
||||||
cert := path.Join(tempDir, "oidc-cert")
|
|
||||||
key := path.Join(tempDir, "oidc-key")
|
|
||||||
defer os.RemoveAll(tempDir)
|
|
||||||
|
|
||||||
oidctesting.GenerateSelfSignedCert(t, "127.0.0.1", cert, key)
|
|
||||||
op := oidctesting.NewOIDCProvider(t, "")
|
|
||||||
srv, err := op.ServeTLSWithKeyPair(cert, key)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Cannot start server %v", err)
|
|
||||||
}
|
|
||||||
defer srv.Close()
|
|
||||||
|
|
||||||
certData, err := ioutil.ReadFile(cert)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Could not read cert bytes %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
makeToken := func(exp time.Time) *jose.JWT {
|
|
||||||
jwt, err := jose.NewSignedJWT(jose.Claims(map[string]interface{}{
|
|
||||||
"exp": exp.UTC().Unix(),
|
|
||||||
}), op.PrivKey.Signer())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Could not create signed JWT %v", err)
|
|
||||||
}
|
|
||||||
return jwt
|
|
||||||
}
|
|
||||||
|
|
||||||
t0 := time.Now()
|
|
||||||
|
|
||||||
goodToken := makeToken(t0.Add(time.Hour)).Encode()
|
|
||||||
expiredToken := makeToken(t0.Add(-time.Hour)).Encode()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
|
|
||||||
cfg map[string]string
|
|
||||||
wantInitErr bool
|
|
||||||
|
|
||||||
client OIDCClient
|
|
||||||
wantCfg map[string]string
|
|
||||||
wantTokenErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
// A Valid configuration
|
|
||||||
name: "no id token and no refresh token",
|
|
||||||
cfg: map[string]string{
|
|
||||||
cfgIssuerUrl: srv.URL,
|
|
||||||
cfgCertificateAuthority: cert,
|
|
||||||
cfgClientID: "client-id",
|
|
||||||
cfgClientSecret: "client-secret",
|
|
||||||
},
|
|
||||||
wantTokenErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "valid config with an initial token",
|
|
||||||
cfg: map[string]string{
|
|
||||||
cfgIssuerUrl: srv.URL,
|
|
||||||
cfgCertificateAuthority: cert,
|
|
||||||
cfgClientID: "client-id",
|
|
||||||
cfgClientSecret: "client-secret",
|
|
||||||
cfgIDToken: goodToken,
|
|
||||||
},
|
|
||||||
client: new(noRefreshOIDCClient),
|
|
||||||
wantCfg: map[string]string{
|
|
||||||
cfgIssuerUrl: srv.URL,
|
|
||||||
cfgCertificateAuthority: cert,
|
|
||||||
cfgClientID: "client-id",
|
|
||||||
cfgClientSecret: "client-secret",
|
|
||||||
cfgIDToken: goodToken,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid ID token with a refresh token",
|
|
||||||
cfg: map[string]string{
|
|
||||||
cfgIssuerUrl: srv.URL,
|
|
||||||
cfgCertificateAuthority: cert,
|
|
||||||
cfgClientID: "client-id",
|
|
||||||
cfgClientSecret: "client-secret",
|
|
||||||
cfgRefreshToken: "foo",
|
|
||||||
cfgIDToken: expiredToken,
|
|
||||||
},
|
|
||||||
client: &mockOIDCClient{
|
|
||||||
tokenResponse: oauth2.TokenResponse{
|
|
||||||
IDToken: goodToken,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantCfg: map[string]string{
|
|
||||||
cfgIssuerUrl: srv.URL,
|
|
||||||
cfgCertificateAuthority: cert,
|
|
||||||
cfgClientID: "client-id",
|
|
||||||
cfgClientSecret: "client-secret",
|
|
||||||
cfgRefreshToken: "foo",
|
|
||||||
cfgIDToken: goodToken,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid ID token with a refresh token, server returns new refresh token",
|
|
||||||
cfg: map[string]string{
|
|
||||||
cfgIssuerUrl: srv.URL,
|
|
||||||
cfgCertificateAuthority: cert,
|
|
||||||
cfgClientID: "client-id",
|
|
||||||
cfgClientSecret: "client-secret",
|
|
||||||
cfgRefreshToken: "foo",
|
|
||||||
cfgIDToken: expiredToken,
|
|
||||||
},
|
|
||||||
client: &mockOIDCClient{
|
|
||||||
tokenResponse: oauth2.TokenResponse{
|
|
||||||
IDToken: goodToken,
|
|
||||||
RefreshToken: "bar",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantCfg: map[string]string{
|
|
||||||
cfgIssuerUrl: srv.URL,
|
|
||||||
cfgCertificateAuthority: cert,
|
|
||||||
cfgClientID: "client-id",
|
|
||||||
cfgClientSecret: "client-secret",
|
|
||||||
cfgRefreshToken: "bar",
|
|
||||||
cfgIDToken: goodToken,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "expired token and no refresh otken",
|
|
||||||
cfg: map[string]string{
|
|
||||||
cfgIssuerUrl: srv.URL,
|
|
||||||
cfgCertificateAuthority: cert,
|
|
||||||
cfgClientID: "client-id",
|
|
||||||
cfgClientSecret: "client-secret",
|
|
||||||
cfgIDToken: expiredToken,
|
|
||||||
},
|
|
||||||
wantTokenErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "valid base64d ca",
|
|
||||||
cfg: map[string]string{
|
|
||||||
cfgIssuerUrl: srv.URL,
|
|
||||||
cfgCertificateAuthorityData: base64.StdEncoding.EncodeToString(certData),
|
|
||||||
cfgClientID: "client-id",
|
|
||||||
cfgClientSecret: "client-secret",
|
|
||||||
},
|
|
||||||
client: new(noRefreshOIDCClient),
|
|
||||||
wantTokenErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing client ID",
|
|
||||||
cfg: map[string]string{
|
|
||||||
cfgIssuerUrl: srv.URL,
|
|
||||||
cfgCertificateAuthority: cert,
|
|
||||||
cfgClientSecret: "client-secret",
|
|
||||||
},
|
|
||||||
wantInitErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing client secret",
|
|
||||||
cfg: map[string]string{
|
|
||||||
cfgIssuerUrl: srv.URL,
|
|
||||||
cfgCertificateAuthority: cert,
|
|
||||||
cfgClientID: "client-id",
|
|
||||||
},
|
|
||||||
wantInitErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing issuer URL",
|
|
||||||
cfg: map[string]string{
|
|
||||||
cfgCertificateAuthority: cert,
|
|
||||||
cfgClientID: "client-id",
|
|
||||||
cfgClientSecret: "secret",
|
|
||||||
},
|
|
||||||
wantInitErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing TLS config",
|
|
||||||
cfg: map[string]string{
|
|
||||||
cfgIssuerUrl: srv.URL,
|
|
||||||
cfgClientID: "client-id",
|
|
||||||
cfgClientSecret: "secret",
|
|
||||||
},
|
|
||||||
wantInitErr: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
clearCache()
|
|
||||||
|
|
||||||
p, err := newOIDCAuthProvider("cluster.example.com", tt.cfg, new(persister))
|
|
||||||
if tt.wantInitErr {
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("%s: want non-nil err", tt.name)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("%s: unexpected error on newOIDCAuthProvider: %v", tt.name, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
provider := p.(*oidcAuthProvider)
|
|
||||||
provider.client = tt.client
|
|
||||||
provider.now = func() time.Time { return t0 }
|
|
||||||
|
|
||||||
if _, err := provider.idToken(); err != nil {
|
|
||||||
if !tt.wantTokenErr {
|
|
||||||
t.Errorf("%s: failed to get id token: %v", tt.name, err)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if tt.wantTokenErr {
|
|
||||||
t.Errorf("%s: expected to not get id token: %v", tt.name, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(tt.wantCfg, provider.cfg) {
|
|
||||||
t.Errorf("%s: expected config %#v got %#v", tt.name, tt.wantCfg, provider.cfg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVerifyJWTExpiry(t *testing.T) {
|
|
||||||
privKey, err := key.GeneratePrivateKey()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("can't generate private key: %v", err)
|
|
||||||
}
|
|
||||||
makeToken := func(s string, exp time.Time, count int) *jose.JWT {
|
|
||||||
jwt, err := jose.NewSignedJWT(jose.Claims(map[string]interface{}{
|
|
||||||
"test": s,
|
|
||||||
"exp": exp.UTC().Unix(),
|
|
||||||
"count": count,
|
|
||||||
}), privKey.Signer())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Could not create signed JWT %v", err)
|
|
||||||
}
|
|
||||||
return jwt
|
|
||||||
}
|
|
||||||
|
|
||||||
t0 := time.Now()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
jwt *jose.JWT
|
|
||||||
now time.Time
|
|
||||||
wantErr bool
|
|
||||||
wantExpired bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "valid jwt",
|
|
||||||
jwt: makeToken("foo", t0.Add(time.Hour), 1),
|
|
||||||
now: t0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid jwt",
|
|
||||||
jwt: &jose.JWT{},
|
|
||||||
now: t0,
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "expired jwt",
|
|
||||||
jwt: makeToken("foo", t0.Add(-time.Hour), 1),
|
|
||||||
now: t0,
|
|
||||||
wantExpired: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "jwt expires soon enough to be marked expired",
|
|
||||||
jwt: makeToken("foo", t0, 1),
|
|
||||||
now: t0,
|
|
||||||
wantExpired: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tests {
|
|
||||||
func() {
|
|
||||||
valid, err := verifyJWTExpiry(tc.now, tc.jwt.Encode())
|
|
||||||
if err != nil {
|
|
||||||
if !tc.wantErr {
|
|
||||||
t.Errorf("%s: %v", tc.name, err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if tc.wantErr {
|
|
||||||
t.Errorf("%s: expected error", tc.name)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if valid && tc.wantExpired {
|
|
||||||
t.Errorf("%s: expected token to be expired", tc.name)
|
|
||||||
}
|
|
||||||
if !valid && !tc.wantExpired {
|
|
||||||
t.Errorf("%s: expected token to be valid", tc.name)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestClientCache(t *testing.T) {
|
|
||||||
cache := newClientCache()
|
|
||||||
|
|
||||||
if _, ok := cache.getClient("issuer1", "id1", "secret1"); ok {
|
|
||||||
t.Fatalf("got client before putting one in the cache")
|
|
||||||
}
|
|
||||||
|
|
||||||
cli1 := new(oidcAuthProvider)
|
|
||||||
cli2 := new(oidcAuthProvider)
|
|
||||||
|
|
||||||
gotcli := cache.setClient("issuer1", "id1", "secret1", cli1)
|
|
||||||
if cli1 != gotcli {
|
|
||||||
t.Fatalf("set first client and got a different one")
|
|
||||||
}
|
|
||||||
|
|
||||||
gotcli = cache.setClient("issuer1", "id1", "secret1", cli2)
|
|
||||||
if cli1 != gotcli {
|
|
||||||
t.Fatalf("set a second client and didn't get the first")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2016 The Kubernetes Authors.
|
|
||||||
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
// Initialize all known client auth plugins.
|
|
||||||
_ "k8s.io/kubernetes/plugin/pkg/client/auth/gcp"
|
|
||||||
_ "k8s.io/kubernetes/plugin/pkg/client/auth/oidc"
|
|
||||||
)
|
|
@ -75,11 +75,14 @@ save "tools/clientcmd/api"
|
|||||||
save "rest"
|
save "rest"
|
||||||
# remove the rest/fake until we're authoritative for it (need to update for registry)
|
# remove the rest/fake until we're authoritative for it (need to update for registry)
|
||||||
rm -rf ${CLIENT_REPO_TEMP}/rest/fake
|
rm -rf ${CLIENT_REPO_TEMP}/rest/fake
|
||||||
|
save "pkg/third_party"
|
||||||
save "pkg/util/cert"
|
save "pkg/util/cert"
|
||||||
save "pkg/util/clock"
|
save "pkg/util/clock"
|
||||||
save "pkg/util/flowcontrol"
|
save "pkg/util/flowcontrol"
|
||||||
save "pkg/util/integer"
|
save "pkg/util/integer"
|
||||||
|
save "pkg/util/jsonpath"
|
||||||
save "pkg/util/testing"
|
save "pkg/util/testing"
|
||||||
|
save "plugin"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -105,7 +108,6 @@ mkcp "/pkg/client/unversioned/auth" "/pkg/client/unversioned"
|
|||||||
mkcp "/pkg/client/unversioned/clientcmd" "/pkg/client/unversioned"
|
mkcp "/pkg/client/unversioned/clientcmd" "/pkg/client/unversioned"
|
||||||
mkcp "/pkg/client/unversioned/portforward" "/pkg/client/unversioned"
|
mkcp "/pkg/client/unversioned/portforward" "/pkg/client/unversioned"
|
||||||
|
|
||||||
mkcp "/plugin/pkg/client/auth" "/plugin/pkg/client"
|
|
||||||
mkcp "/pkg/util/workqueue" "pkg/util"
|
mkcp "/pkg/util/workqueue" "pkg/util"
|
||||||
# remove this folder because it imports prometheus
|
# remove this folder because it imports prometheus
|
||||||
rm -rf "${CLIENT_REPO_TEMP}/pkg/util/workqueue/prometheus"
|
rm -rf "${CLIENT_REPO_TEMP}/pkg/util/workqueue/prometheus"
|
||||||
@ -208,7 +210,6 @@ if [ "$(find "${CLIENT_REPO_TEMP}"/pkg/client -type f -name "*.go")" ]; then
|
|||||||
else
|
else
|
||||||
rm -r "${CLIENT_REPO_TEMP}"/pkg/client
|
rm -r "${CLIENT_REPO_TEMP}"/pkg/client
|
||||||
fi
|
fi
|
||||||
mvfolder third_party pkg/third_party
|
|
||||||
mvfolder federation pkg/federation
|
mvfolder federation pkg/federation
|
||||||
|
|
||||||
echo "running gofmt"
|
echo "running gofmt"
|
||||||
|
@ -17,4 +17,4 @@ limitations under the License.
|
|||||||
// package jsonpath is a template engine using jsonpath syntax,
|
// package jsonpath is a template engine using jsonpath syntax,
|
||||||
// which can be seen at http://goessner.net/articles/JsonPath/.
|
// which can be seen at http://goessner.net/articles/JsonPath/.
|
||||||
// In addition, it has {range} {end} function to iterate list and slice.
|
// In addition, it has {range} {end} function to iterate list and slice.
|
||||||
package jsonpath
|
package jsonpath // import "k8s.io/client-go/pkg/util/jsonpath"
|
||||||
|
@ -30,7 +30,7 @@ import (
|
|||||||
"github.com/coreos/go-oidc/key"
|
"github.com/coreos/go-oidc/key"
|
||||||
"github.com/coreos/go-oidc/oauth2"
|
"github.com/coreos/go-oidc/oauth2"
|
||||||
|
|
||||||
oidctesting "k8s.io/client-go/plugin/pkg/auth/authenticator/token/oidc/testing"
|
oidctesting "k8s.io/kubernetes/plugin/pkg/auth/authenticator/token/oidc/testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func clearCache() {
|
func clearCache() {
|
||||||
|
Loading…
Reference in New Issue
Block a user