Add OIDC token generation to Identity (#6900)
* Add OIDC token generation to Identity There are a few open TODOs and some remaining cleanup, but this is functionally complete and ready for review. (Tests will being added soon.) * Simplified key update endpoint * Cache the config * Fix Issuer handling * Suppose base64-encoded templates (#6919) * Cache JWKS and switch to go-cache (#6918) * Address review comments * Add warning if neither Issue nor api_addr are set * adds tests (#6937) * adds help synopsis and descriptions to the framework path for the oid… (#6930) * adds help synopsis and descriptions to the framework path for the oidc backend * Update vault/identity_store_oidc.go Co-Authored-By: Jim Kalafut <jim@kalafut.net> * Add Now parameter to PopulateStringInput * Addressing review comments * Refactor template processing to improve mode-specific handling * adds a test for the periodic func (#6943) * adds a test for the periodic func * removes commented out code * adds a comment * Add comments
This commit is contained in:
parent
5d0c68ca74
commit
2bf5db4fe8
|
@ -1,10 +1,14 @@
|
|||
package identity
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/errwrap"
|
||||
"github.com/hashicorp/vault/helper/namespace"
|
||||
)
|
||||
|
||||
|
@ -15,23 +19,106 @@ var (
|
|||
ErrTemplateValueNotFound = errors.New("no value could be found for one of the template directives")
|
||||
)
|
||||
|
||||
const (
|
||||
ACLTemplating = iota // must be the first value for backwards compatibility
|
||||
JSONTemplating
|
||||
)
|
||||
|
||||
type PopulateStringInput struct {
|
||||
ValidityCheckOnly bool
|
||||
String string
|
||||
ValidityCheckOnly bool
|
||||
Entity *Entity
|
||||
Groups []*Group
|
||||
Namespace *namespace.Namespace
|
||||
Mode int // processing mode, ACLTemplate or JSONTemplating
|
||||
Now time.Time // optional, defaults to current time
|
||||
|
||||
templateHandler templateHandlerFunc
|
||||
groupIDs []string
|
||||
groupNames []string
|
||||
}
|
||||
|
||||
func PopulateString(p *PopulateStringInput) (bool, string, error) {
|
||||
if p == nil {
|
||||
return false, "", errors.New("nil input")
|
||||
// templateHandlerFunc allows generating string outputs based on data type, and
|
||||
// different handlers can be used based on mode. For example in ACL mode, strings
|
||||
// are emitted verbatim, but they're wrapped in double quotes for JSON mode. And
|
||||
// some structures, like slices, might be rendered in one mode but prohibited in
|
||||
// another.
|
||||
type templateHandlerFunc func(interface{}, ...string) (string, error)
|
||||
|
||||
// aclTemplateHandler processes known parameter data types when operating
|
||||
// in ACL mode.
|
||||
func aclTemplateHandler(v interface{}, keys ...string) (string, error) {
|
||||
switch t := v.(type) {
|
||||
case string:
|
||||
if t == "" {
|
||||
return "", ErrTemplateValueNotFound
|
||||
}
|
||||
return t, nil
|
||||
case []string:
|
||||
return "", ErrTemplateValueNotFound
|
||||
case map[string]string:
|
||||
if len(keys) > 0 {
|
||||
val, ok := t[keys[0]]
|
||||
if ok {
|
||||
return val, nil
|
||||
}
|
||||
}
|
||||
return "", ErrTemplateValueNotFound
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("unknown type: %T", v)
|
||||
}
|
||||
|
||||
// jsonTemplateHandler processes known parameter data types when operating
|
||||
// in JSON mode.
|
||||
func jsonTemplateHandler(v interface{}, keys ...string) (string, error) {
|
||||
jsonMarshaller := func(v interface{}) (string, error) {
|
||||
enc, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(enc), nil
|
||||
}
|
||||
|
||||
switch t := v.(type) {
|
||||
case string:
|
||||
return strconv.Quote(t), nil
|
||||
case []string:
|
||||
return jsonMarshaller(t)
|
||||
case map[string]string:
|
||||
if len(keys) > 0 {
|
||||
return strconv.Quote(t[keys[0]]), nil
|
||||
}
|
||||
if t == nil {
|
||||
return "{}", nil
|
||||
}
|
||||
return jsonMarshaller(t)
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("unknown type: %T", v)
|
||||
}
|
||||
|
||||
func PopulateString(p PopulateStringInput) (bool, string, error) {
|
||||
if p.String == "" {
|
||||
return false, "", nil
|
||||
}
|
||||
|
||||
// preprocess groups
|
||||
for _, g := range p.Groups {
|
||||
p.groupNames = append(p.groupNames, g.Name)
|
||||
p.groupIDs = append(p.groupIDs, g.ID)
|
||||
}
|
||||
|
||||
// set up mode-specific handler
|
||||
switch p.Mode {
|
||||
case ACLTemplating:
|
||||
p.templateHandler = aclTemplateHandler
|
||||
case JSONTemplating:
|
||||
p.templateHandler = jsonTemplateHandler
|
||||
default:
|
||||
return false, "", fmt.Errorf("unknown mode %q", p.Mode)
|
||||
}
|
||||
|
||||
var subst bool
|
||||
splitStr := strings.Split(p.String, "{{")
|
||||
|
||||
|
@ -61,7 +148,7 @@ func PopulateString(p *PopulateStringInput) (bool, string, error) {
|
|||
case 2:
|
||||
subst = true
|
||||
if !p.ValidityCheckOnly {
|
||||
tmplStr, err := performTemplating(p.Namespace, strings.TrimSpace(splitPiece[0]), p.Entity, p.Groups)
|
||||
tmplStr, err := performTemplating(strings.TrimSpace(splitPiece[0]), &p)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
|
@ -76,22 +163,22 @@ func PopulateString(p *PopulateStringInput) (bool, string, error) {
|
|||
return subst, b.String(), nil
|
||||
}
|
||||
|
||||
func performTemplating(ns *namespace.Namespace, input string, entity *Entity, groups []*Group) (string, error) {
|
||||
func performTemplating(input string, p *PopulateStringInput) (string, error) {
|
||||
|
||||
performAliasTemplating := func(trimmed string, alias *Alias) (string, error) {
|
||||
switch {
|
||||
case trimmed == "id":
|
||||
return alias.ID, nil
|
||||
return p.templateHandler(alias.ID)
|
||||
|
||||
case trimmed == "name":
|
||||
if alias.Name == "" {
|
||||
return "", ErrTemplateValueNotFound
|
||||
}
|
||||
return alias.Name, nil
|
||||
return p.templateHandler(alias.Name)
|
||||
|
||||
case trimmed == "metadata":
|
||||
return p.templateHandler(alias.Metadata)
|
||||
|
||||
case strings.HasPrefix(trimmed, "metadata."):
|
||||
val, ok := alias.Metadata[strings.TrimPrefix(trimmed, "metadata.")]
|
||||
if !ok {
|
||||
return "", ErrTemplateValueNotFound
|
||||
}
|
||||
return val, nil
|
||||
split := strings.SplitN(trimmed, ".", 2)
|
||||
return p.templateHandler(alias.Metadata, split[1])
|
||||
}
|
||||
|
||||
return "", ErrTemplateValueNotFound
|
||||
|
@ -100,34 +187,45 @@ func performTemplating(ns *namespace.Namespace, input string, entity *Entity, gr
|
|||
performEntityTemplating := func(trimmed string) (string, error) {
|
||||
switch {
|
||||
case trimmed == "id":
|
||||
return entity.ID, nil
|
||||
return p.templateHandler(p.Entity.ID)
|
||||
|
||||
case trimmed == "name":
|
||||
if entity.Name == "" {
|
||||
return "", ErrTemplateValueNotFound
|
||||
}
|
||||
return entity.Name, nil
|
||||
return p.templateHandler(p.Entity.Name)
|
||||
|
||||
case trimmed == "metadata":
|
||||
return p.templateHandler(p.Entity.Metadata)
|
||||
|
||||
case strings.HasPrefix(trimmed, "metadata."):
|
||||
val, ok := entity.Metadata[strings.TrimPrefix(trimmed, "metadata.")]
|
||||
if !ok {
|
||||
return "", ErrTemplateValueNotFound
|
||||
}
|
||||
return val, nil
|
||||
split := strings.SplitN(trimmed, ".", 2)
|
||||
return p.templateHandler(p.Entity.Metadata, split[1])
|
||||
|
||||
case trimmed == "group_names":
|
||||
return p.templateHandler(p.groupNames)
|
||||
|
||||
case trimmed == "group_ids":
|
||||
return p.templateHandler(p.groupIDs)
|
||||
|
||||
case strings.HasPrefix(trimmed, "aliases."):
|
||||
split := strings.SplitN(strings.TrimPrefix(trimmed, "aliases."), ".", 2)
|
||||
if len(split) != 2 {
|
||||
return "", errors.New("invalid alias selector")
|
||||
}
|
||||
var found *Alias
|
||||
for _, alias := range entity.Aliases {
|
||||
if split[0] == alias.MountAccessor {
|
||||
found = alias
|
||||
var alias *Alias
|
||||
for _, a := range p.Entity.Aliases {
|
||||
if split[0] == a.MountAccessor {
|
||||
alias = a
|
||||
break
|
||||
}
|
||||
}
|
||||
if found == nil {
|
||||
return "", errors.New("alias not found")
|
||||
if alias == nil {
|
||||
if p.Mode == ACLTemplating {
|
||||
return "", errors.New("alias not found")
|
||||
}
|
||||
|
||||
// An empty alias is sufficient for generating defaults
|
||||
alias = &Alias{Metadata: make(map[string]string)}
|
||||
}
|
||||
return performAliasTemplating(split[1], found)
|
||||
return performAliasTemplating(split[1], alias)
|
||||
}
|
||||
|
||||
return "", ErrTemplateValueNotFound
|
||||
|
@ -137,12 +235,16 @@ func performTemplating(ns *namespace.Namespace, input string, entity *Entity, gr
|
|||
var ids bool
|
||||
|
||||
selectorSplit := strings.SplitN(trimmed, ".", 2)
|
||||
|
||||
switch {
|
||||
case len(selectorSplit) != 2:
|
||||
return "", errors.New("invalid groups selector")
|
||||
|
||||
case selectorSplit[0] == "ids":
|
||||
ids = true
|
||||
|
||||
case selectorSplit[0] == "names":
|
||||
|
||||
default:
|
||||
return "", errors.New("invalid groups selector")
|
||||
}
|
||||
|
@ -153,12 +255,12 @@ func performTemplating(ns *namespace.Namespace, input string, entity *Entity, gr
|
|||
return "", errors.New("invalid groups accessor")
|
||||
}
|
||||
var found *Group
|
||||
for _, group := range groups {
|
||||
for _, group := range p.Groups {
|
||||
var compare string
|
||||
if ids {
|
||||
compare = group.ID
|
||||
} else {
|
||||
if ns != nil && group.NamespaceID == ns.ID {
|
||||
if p.Namespace != nil && group.NamespaceID == p.Namespace.ID {
|
||||
compare = group.Name
|
||||
} else {
|
||||
continue
|
||||
|
@ -180,11 +282,13 @@ func performTemplating(ns *namespace.Namespace, input string, entity *Entity, gr
|
|||
switch {
|
||||
case trimmed == "id":
|
||||
return found.ID, nil
|
||||
|
||||
case trimmed == "name":
|
||||
if found.Name == "" {
|
||||
return "", ErrTemplateValueNotFound
|
||||
}
|
||||
return found.Name, nil
|
||||
|
||||
case strings.HasPrefix(trimmed, "metadata."):
|
||||
val, ok := found.Metadata[strings.TrimPrefix(trimmed, "metadata.")]
|
||||
if !ok {
|
||||
|
@ -196,18 +300,59 @@ func performTemplating(ns *namespace.Namespace, input string, entity *Entity, gr
|
|||
return "", ErrTemplateValueNotFound
|
||||
}
|
||||
|
||||
performTimeTemplating := func(trimmed string) (string, error) {
|
||||
now := p.Now
|
||||
if now.IsZero() {
|
||||
now = time.Now()
|
||||
}
|
||||
|
||||
opsSplit := strings.SplitN(trimmed, ".", 3)
|
||||
|
||||
if opsSplit[0] != "now" {
|
||||
return "", fmt.Errorf("invalid time selector %q", opsSplit[0])
|
||||
}
|
||||
|
||||
result := now
|
||||
switch len(opsSplit) {
|
||||
case 1:
|
||||
// return current time
|
||||
case 2:
|
||||
return "", errors.New("missing time operand")
|
||||
|
||||
case 3:
|
||||
duration, err := time.ParseDuration(opsSplit[2])
|
||||
if err != nil {
|
||||
return "", errwrap.Wrapf("invalid duration: {{err}}", err)
|
||||
}
|
||||
|
||||
switch opsSplit[1] {
|
||||
case "plus":
|
||||
result = result.Add(duration)
|
||||
case "minus":
|
||||
result = result.Add(-duration)
|
||||
default:
|
||||
return "", fmt.Errorf("invalid time operator %q", opsSplit[1])
|
||||
}
|
||||
}
|
||||
|
||||
return strconv.FormatInt(result.Unix(), 10), nil
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(input, "identity.entity."):
|
||||
if entity == nil {
|
||||
if p.Entity == nil {
|
||||
return "", ErrNoEntityAttachedToToken
|
||||
}
|
||||
return performEntityTemplating(strings.TrimPrefix(input, "identity.entity."))
|
||||
|
||||
case strings.HasPrefix(input, "identity.groups."):
|
||||
if len(groups) == 0 {
|
||||
if len(p.Groups) == 0 {
|
||||
return "", ErrNoGroupsAttachedToToken
|
||||
}
|
||||
return performGroupsTemplating(strings.TrimPrefix(input, "identity.groups."))
|
||||
|
||||
case strings.HasPrefix(input, "time."):
|
||||
return performTimeTemplating(strings.TrimPrefix(input, "time."))
|
||||
}
|
||||
|
||||
return "", ErrTemplateValueNotFound
|
||||
|
|
|
@ -2,13 +2,22 @@ package identity
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/vault/helper/namespace"
|
||||
)
|
||||
|
||||
// intentionally != time.Now() to catch latent used of time.Now instead of
|
||||
// passed in values
|
||||
var testNow = time.Now().Add(100 * time.Hour)
|
||||
|
||||
func TestPopulate_Basic(t *testing.T) {
|
||||
var tests = []struct {
|
||||
mode int
|
||||
name string
|
||||
input string
|
||||
output string
|
||||
|
@ -23,7 +32,40 @@ func TestPopulate_Basic(t *testing.T) {
|
|||
aliasMetadata map[string]string
|
||||
groupName string
|
||||
groupMetadata map[string]string
|
||||
groupMemberships []string
|
||||
now time.Time
|
||||
}{
|
||||
// time.* tests. Keep tests with time.Now() at the front to avoid false
|
||||
// positives due to the second changing during the test
|
||||
{
|
||||
name: "time now",
|
||||
input: "{{time.now}}",
|
||||
output: strconv.Itoa(int(testNow.Unix())),
|
||||
now: testNow,
|
||||
},
|
||||
{
|
||||
name: "time plus",
|
||||
input: "{{time.now.plus.1h}}",
|
||||
output: strconv.Itoa(int(testNow.Unix() + (60 * 60))),
|
||||
now: testNow,
|
||||
},
|
||||
{
|
||||
name: "time plus",
|
||||
input: "{{time.now.minus.5m}}",
|
||||
output: strconv.Itoa(int(testNow.Unix() - (5 * 60))),
|
||||
now: testNow,
|
||||
},
|
||||
{
|
||||
name: "invalid operator",
|
||||
input: "{{time.now.divide.5m}}",
|
||||
err: errors.New("invalid time operator \"divide\""),
|
||||
},
|
||||
{
|
||||
name: "time missing operand",
|
||||
input: "{{time.now.plus}}",
|
||||
err: errors.New("missing time operand"),
|
||||
},
|
||||
|
||||
{
|
||||
name: "no_templating",
|
||||
input: "path foobar {",
|
||||
|
@ -86,12 +128,13 @@ func TestPopulate_Basic(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "alias_id_name",
|
||||
input: "path {{ identity.entity.name}} {\n\tval = {{identity.entity.aliases.foomount.id}}\n}",
|
||||
input: "path {{ identity.entity.name}} {\n\tval = {{identity.entity.aliases.foomount.id}} nval = {{identity.entity.aliases.foomount.name}}\n}",
|
||||
entityName: "entityName",
|
||||
aliasAccessor: "foomount",
|
||||
aliasID: "aliasID",
|
||||
aliasName: "aliasName",
|
||||
metadata: map[string]string{"foo": "bar"},
|
||||
output: "path entityName {\n\tval = aliasID\n}",
|
||||
output: "path entityName {\n\tval = aliasID nval = aliasName\n}",
|
||||
},
|
||||
{
|
||||
name: "alias_id_name_bad_selector",
|
||||
|
@ -143,6 +186,149 @@ func TestPopulate_Basic(t *testing.T) {
|
|||
groupName: "groupName",
|
||||
err: errors.New("entity is not a member of group \"hroupName\""),
|
||||
},
|
||||
{
|
||||
name: "metadata_object_disallowed",
|
||||
input: "{{identity.entity.metadata}}",
|
||||
metadata: map[string]string{"foo": "bar"},
|
||||
err: ErrTemplateValueNotFound,
|
||||
},
|
||||
{
|
||||
name: "alias_metadata_object_disallowed",
|
||||
input: "{{identity.entity.aliases.foomount.metadata}}",
|
||||
aliasAccessor: "foomount",
|
||||
aliasMetadata: map[string]string{"foo": "bar"},
|
||||
err: ErrTemplateValueNotFound,
|
||||
},
|
||||
{
|
||||
name: "group_names_disallowed",
|
||||
input: "{{identity.entity.group_names}}",
|
||||
groupMemberships: []string{"foo", "bar"},
|
||||
err: ErrTemplateValueNotFound,
|
||||
},
|
||||
{
|
||||
name: "group_ids_disallowed",
|
||||
input: "{{identity.entity.group_ids}}",
|
||||
groupMemberships: []string{"foo", "bar"},
|
||||
err: ErrTemplateValueNotFound,
|
||||
},
|
||||
|
||||
// missing selector cases
|
||||
{
|
||||
mode: JSONTemplating,
|
||||
name: "entity id",
|
||||
input: "{{identity.entity.id}}",
|
||||
output: `"entityID"`,
|
||||
},
|
||||
{
|
||||
mode: JSONTemplating,
|
||||
name: "entity name",
|
||||
input: "{{identity.entity.name}}",
|
||||
entityName: "entityName",
|
||||
output: `"entityName"`,
|
||||
},
|
||||
{
|
||||
mode: JSONTemplating,
|
||||
name: "entity name missing",
|
||||
input: "{{identity.entity.name}}",
|
||||
output: `""`,
|
||||
},
|
||||
{
|
||||
mode: JSONTemplating,
|
||||
name: "alias name/id",
|
||||
input: "{{identity.entity.aliases.foomount.id}} {{identity.entity.aliases.foomount.name}}",
|
||||
aliasAccessor: "foomount",
|
||||
aliasID: "aliasID",
|
||||
aliasName: "aliasName",
|
||||
output: `"aliasID" "aliasName"`,
|
||||
},
|
||||
{
|
||||
mode: JSONTemplating,
|
||||
name: "one metadata key",
|
||||
input: "{{identity.entity.metadata.color}}",
|
||||
metadata: map[string]string{"foo": "bar", "color": "green"},
|
||||
output: `"green"`,
|
||||
},
|
||||
{
|
||||
mode: JSONTemplating,
|
||||
name: "one metadata key not found",
|
||||
input: "{{identity.entity.metadata.size}}",
|
||||
metadata: map[string]string{"foo": "bar", "color": "green"},
|
||||
output: `""`,
|
||||
},
|
||||
{
|
||||
mode: JSONTemplating,
|
||||
name: "all entity metadata",
|
||||
input: "{{identity.entity.metadata}}",
|
||||
metadata: map[string]string{"foo": "bar", "color": "green"},
|
||||
output: `{"color":"green","foo":"bar"}`,
|
||||
},
|
||||
{
|
||||
mode: JSONTemplating,
|
||||
name: "null entity metadata",
|
||||
input: "{{identity.entity.metadata}}",
|
||||
output: `{}`,
|
||||
},
|
||||
{
|
||||
mode: JSONTemplating,
|
||||
name: "group_names",
|
||||
input: "{{identity.entity.group_names}}",
|
||||
groupMemberships: []string{"foo", "bar"},
|
||||
output: `["foo","bar"]`,
|
||||
},
|
||||
{
|
||||
mode: JSONTemplating,
|
||||
name: "group_ids",
|
||||
input: "{{identity.entity.group_ids}}",
|
||||
groupMemberships: []string{"foo", "bar"},
|
||||
output: `["foo_0","bar_1"]`,
|
||||
},
|
||||
{
|
||||
mode: JSONTemplating,
|
||||
name: "one alias metadata key",
|
||||
input: "{{identity.entity.aliases.aws_123.metadata.color}}",
|
||||
aliasAccessor: "aws_123",
|
||||
aliasMetadata: map[string]string{"foo": "bar", "color": "green"},
|
||||
output: `"green"`,
|
||||
},
|
||||
{
|
||||
mode: JSONTemplating,
|
||||
name: "one alias metadata key not found",
|
||||
input: "{{identity.entity.aliases.aws_123.metadata.size}}",
|
||||
aliasAccessor: "aws_123",
|
||||
aliasMetadata: map[string]string{"foo": "bar", "color": "green"},
|
||||
output: `""`,
|
||||
},
|
||||
{
|
||||
mode: JSONTemplating,
|
||||
name: "one alias metadata, accessor not found",
|
||||
input: "{{identity.entity.aliases.aws_123.metadata.size}}",
|
||||
aliasAccessor: "not_gonna_match",
|
||||
aliasMetadata: map[string]string{"foo": "bar", "color": "green"},
|
||||
output: `""`,
|
||||
},
|
||||
{
|
||||
mode: JSONTemplating,
|
||||
name: "all alias metadata",
|
||||
input: "{{identity.entity.aliases.aws_123.metadata}}",
|
||||
aliasAccessor: "aws_123",
|
||||
aliasMetadata: map[string]string{"foo": "bar", "color": "green"},
|
||||
output: `{"color":"green","foo":"bar"}`,
|
||||
},
|
||||
{
|
||||
mode: JSONTemplating,
|
||||
name: "null alias metadata",
|
||||
input: "{{identity.entity.aliases.aws_123.metadata}}",
|
||||
aliasAccessor: "aws_123",
|
||||
output: `{}`,
|
||||
},
|
||||
{
|
||||
mode: JSONTemplating,
|
||||
name: "all alias metadata, accessor not found",
|
||||
input: "{{identity.entity.aliases.aws_123.metadata}}",
|
||||
aliasAccessor: "not_gonna_match",
|
||||
aliasMetadata: map[string]string{"foo": "bar", "color": "green"},
|
||||
output: `{}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
@ -156,7 +342,7 @@ func TestPopulate_Basic(t *testing.T) {
|
|||
}
|
||||
if test.aliasAccessor != "" {
|
||||
entity.Aliases = []*Alias{
|
||||
&Alias{
|
||||
{
|
||||
MountAccessor: test.aliasAccessor,
|
||||
ID: test.aliasID,
|
||||
Name: test.aliasName,
|
||||
|
@ -173,12 +359,24 @@ func TestPopulate_Basic(t *testing.T) {
|
|||
NamespaceID: namespace.RootNamespace.ID,
|
||||
})
|
||||
}
|
||||
subst, out, err := PopulateString(&PopulateStringInput{
|
||||
|
||||
if test.groupMemberships != nil {
|
||||
for i, groupName := range test.groupMemberships {
|
||||
groups = append(groups, &Group{
|
||||
ID: fmt.Sprintf("%s_%d", groupName, i),
|
||||
Name: groupName,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
subst, out, err := PopulateString(PopulateStringInput{
|
||||
Mode: test.mode,
|
||||
ValidityCheckOnly: test.validityCheckOnly,
|
||||
String: test.input,
|
||||
Entity: entity,
|
||||
Groups: groups,
|
||||
Namespace: namespace.RootNamespace,
|
||||
Now: test.now,
|
||||
})
|
||||
if err != nil {
|
||||
if test.err == nil {
|
||||
|
@ -189,10 +387,108 @@ func TestPopulate_Basic(t *testing.T) {
|
|||
}
|
||||
}
|
||||
if out != test.output {
|
||||
t.Fatalf("%s: bad output: %s", test.name, out)
|
||||
t.Fatalf("%s: bad output: %s, expected: %s", test.name, out, test.output)
|
||||
}
|
||||
if err == nil && !subst && out != test.input {
|
||||
t.Fatalf("%s: bad subst flag", test.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPopulate_CurrentTime(t *testing.T) {
|
||||
now := time.Now()
|
||||
|
||||
// Test that an unset Now parameter results in current time
|
||||
input := PopulateStringInput{
|
||||
Mode: JSONTemplating,
|
||||
String: `{{time.now}}`,
|
||||
}
|
||||
|
||||
_, out, err := PopulateString(input)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
nowPopulated, err := strconv.Atoi(out)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
diff := math.Abs(float64(int64(nowPopulated) - now.Unix()))
|
||||
if diff > 1 {
|
||||
t.Fatalf("expected time within 1 second. Got diff of: %f", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPopulate_FullObject(t *testing.T) {
|
||||
var testEntity = &Entity{
|
||||
ID: "abc-123",
|
||||
Name: "Entity Name",
|
||||
Metadata: map[string]string{
|
||||
"color": "green",
|
||||
"size": "small",
|
||||
"non-printable": "\"\n\t",
|
||||
},
|
||||
Aliases: []*Alias{
|
||||
{
|
||||
MountAccessor: "aws_123",
|
||||
Metadata: map[string]string{
|
||||
"service": "ec2",
|
||||
"region": "west",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var testGroups = []*Group{
|
||||
{ID: "a08b0c02", Name: "g1"},
|
||||
{ID: "239bef91", Name: "g2"},
|
||||
}
|
||||
|
||||
template := `
|
||||
{
|
||||
"id": {{identity.entity.id}},
|
||||
"name": {{identity.entity.name}},
|
||||
"all metadata": {{identity.entity.metadata}},
|
||||
"one metadata key": {{identity.entity.metadata.color}},
|
||||
"one metadata key not found": {{identity.entity.metadata.asldfk}},
|
||||
"alias metadata": {{identity.entity.aliases.aws_123.metadata}},
|
||||
"alias not found metadata": {{identity.entity.aliases.blahblah.metadata}},
|
||||
"one alias metadata key": {{identity.entity.aliases.aws_123.metadata.service}},
|
||||
"one not found alias metadata key": {{identity.entity.aliases.blahblah.metadata.service}},
|
||||
"group names": {{identity.entity.group_names}},
|
||||
"group ids": {{identity.entity.group_ids}},
|
||||
"repeated and": {"nested element": {{identity.entity.name}}}
|
||||
}`
|
||||
|
||||
expected := `
|
||||
{
|
||||
"id": "abc-123",
|
||||
"name": "Entity Name",
|
||||
"all metadata": {"color":"green","non-printable":"\"\n\t","size":"small"},
|
||||
"one metadata key": "green",
|
||||
"one metadata key not found": "",
|
||||
"alias metadata": {"region":"west","service":"ec2"},
|
||||
"alias not found metadata": {},
|
||||
"one alias metadata key": "ec2",
|
||||
"one not found alias metadata key": "",
|
||||
"group names": ["g1","g2"],
|
||||
"group ids": ["a08b0c02","239bef91"],
|
||||
"repeated and": {"nested element": "Entity Name"}
|
||||
}`
|
||||
|
||||
input := PopulateStringInput{
|
||||
Mode: JSONTemplating,
|
||||
String: template,
|
||||
Entity: testEntity,
|
||||
Groups: testGroups,
|
||||
}
|
||||
_, out, err := PopulateString(input)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if out != expected {
|
||||
t.Fatalf("expected:\n%s\n\ngot:\n%s", expected, out)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,10 +5,12 @@ import (
|
|||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/patrickmn/go-cache"
|
||||
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
"github.com/hashicorp/errwrap"
|
||||
log "github.com/hashicorp/go-hclog"
|
||||
memdb "github.com/hashicorp/go-memdb"
|
||||
"github.com/hashicorp/go-memdb"
|
||||
"github.com/hashicorp/vault/helper/identity"
|
||||
"github.com/hashicorp/vault/helper/namespace"
|
||||
"github.com/hashicorp/vault/helper/storagepacker"
|
||||
|
@ -75,8 +77,20 @@ func NewIdentityStore(ctx context.Context, core *Core, config *logical.BackendCo
|
|||
BackendType: logical.TypeLogical,
|
||||
Paths: iStore.paths(),
|
||||
Invalidate: iStore.Invalidate,
|
||||
PathsSpecial: &logical.Paths{
|
||||
Unauthenticated: []string{
|
||||
"oidc/.well-known/*",
|
||||
},
|
||||
},
|
||||
PeriodicFunc: func(ctx context.Context, req *logical.Request) error {
|
||||
iStore.oidcPeriodicFunc(ctx, req.Storage)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
iStore.oidcCache = cache.New(cache.NoExpiration, cache.NoExpiration)
|
||||
|
||||
err = iStore.Setup(ctx, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -93,6 +107,7 @@ func (i *IdentityStore) paths() []*framework.Path {
|
|||
groupPaths(i),
|
||||
lookupPaths(i),
|
||||
upgradePaths(i),
|
||||
oidcPaths(i),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -247,6 +262,9 @@ func (i *IdentityStore) Invalidate(ctx context.Context, key string) {
|
|||
|
||||
txn.Commit()
|
||||
return
|
||||
|
||||
case strings.HasPrefix(key, oidcTokensPrefix):
|
||||
i.oidcCache.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,606 @@
|
|||
package vault
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-test/deep"
|
||||
uuid "github.com/hashicorp/go-uuid"
|
||||
"github.com/hashicorp/vault/helper/identity"
|
||||
"github.com/hashicorp/vault/helper/namespace"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
"gopkg.in/square/go-jose.v2/jwt"
|
||||
)
|
||||
|
||||
// TestOIDC_Path_OIDCRoleRole tests CRUD operations for roles
|
||||
func TestOIDC_Path_OIDCRoleRole(t *testing.T) {
|
||||
c, _, _ := TestCoreUnsealed(t)
|
||||
ctx := namespace.RootContext(nil)
|
||||
storage := &logical.InmemStorage{}
|
||||
|
||||
// Create a test key "test-key"
|
||||
c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/key/test-key",
|
||||
Operation: logical.CreateOperation,
|
||||
Storage: storage,
|
||||
})
|
||||
|
||||
// Create a test role "test-role1" with a valid key -- should succeed
|
||||
resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/role/test-role1",
|
||||
Operation: logical.CreateOperation,
|
||||
Data: map[string]interface{}{
|
||||
"key": "test-key",
|
||||
},
|
||||
Storage: storage,
|
||||
})
|
||||
expectSuccess(t, resp, err)
|
||||
|
||||
// Read "test-role1"
|
||||
respReadTestRole1, err1 := c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/role/test-role1",
|
||||
Operation: logical.ReadOperation,
|
||||
Storage: storage,
|
||||
})
|
||||
expectSuccess(t, respReadTestRole1, err1)
|
||||
|
||||
// Create a test role "test-role2" witn an invalid key -- should fail
|
||||
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/role/test-role2",
|
||||
Operation: logical.CreateOperation,
|
||||
Data: map[string]interface{}{
|
||||
"key": "a-key-that-does-not-exist",
|
||||
},
|
||||
Storage: storage,
|
||||
})
|
||||
expectError(t, resp, err)
|
||||
|
||||
// Update "test-role1" with valid parameters -- should succeed
|
||||
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/role/test-role1",
|
||||
Operation: logical.UpdateOperation,
|
||||
Data: map[string]interface{}{
|
||||
"template": "{\"some-key\":\"some-value\"}",
|
||||
"ttl": "2h",
|
||||
},
|
||||
Storage: storage,
|
||||
})
|
||||
expectSuccess(t, resp, err)
|
||||
|
||||
// Read "test-role1" again
|
||||
respReadTestRole1AfterUpdate, err2 := c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/role/test-role1",
|
||||
Operation: logical.ReadOperation,
|
||||
Storage: storage,
|
||||
})
|
||||
expectSuccess(t, respReadTestRole1AfterUpdate, err2)
|
||||
|
||||
// Compare response for "test-role1" before and after it was updated
|
||||
expectedDiff := map[string]interface{}{
|
||||
"Data.map[template]: != {\"some-key\":\"some-value\"}": true,
|
||||
"Data.map[ttl]: 86400 != 7200": true, // 24h to 2h
|
||||
}
|
||||
diff := deep.Equal(respReadTestRole1, respReadTestRole1AfterUpdate)
|
||||
expectStrings(t, diff, expectedDiff)
|
||||
|
||||
// Delete "test-role1"
|
||||
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/role/test-role1",
|
||||
Operation: logical.DeleteOperation,
|
||||
Storage: storage,
|
||||
})
|
||||
expectSuccess(t, resp, err)
|
||||
|
||||
// Read "test-role1" again
|
||||
respReadTestRole1AfterDelete, err3 := c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/role/test-role1",
|
||||
Operation: logical.ReadOperation,
|
||||
Storage: storage,
|
||||
})
|
||||
// Ensure that "test-role1" has been deleted
|
||||
expectSuccess(t, respReadTestRole1AfterDelete, err3)
|
||||
if respReadTestRole1AfterDelete != nil {
|
||||
t.Fatalf("Expected a nil response but instead got:\n%#v", respReadTestRole1AfterDelete)
|
||||
}
|
||||
if respReadTestRole1AfterDelete != nil {
|
||||
t.Fatalf("Expected role to have been deleted but read response was:\n%#v", respReadTestRole1AfterDelete)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOIDC_Path_OIDCRole tests the List operation for roles
|
||||
func TestOIDC_Path_OIDCRole(t *testing.T) {
|
||||
c, _, _ := TestCoreUnsealed(t)
|
||||
ctx := namespace.RootContext(nil)
|
||||
storage := &logical.InmemStorage{}
|
||||
|
||||
// Prepare two roles, test-role1 and test-role2
|
||||
// Create a test key "test-key"
|
||||
c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/key/test-key",
|
||||
Operation: logical.CreateOperation,
|
||||
Storage: storage,
|
||||
})
|
||||
|
||||
// Create "test-role1"
|
||||
c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/role/test-role1",
|
||||
Operation: logical.CreateOperation,
|
||||
Data: map[string]interface{}{
|
||||
"key": "test-key",
|
||||
},
|
||||
Storage: storage,
|
||||
})
|
||||
|
||||
// Create "test-role2"
|
||||
c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/role/test-role2",
|
||||
Operation: logical.CreateOperation,
|
||||
Data: map[string]interface{}{
|
||||
"key": "test-key",
|
||||
},
|
||||
Storage: storage,
|
||||
})
|
||||
|
||||
// list roles
|
||||
respListRole, listErr := c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/role",
|
||||
Operation: logical.ListOperation,
|
||||
Storage: storage,
|
||||
})
|
||||
expectSuccess(t, respListRole, listErr)
|
||||
|
||||
// validate list response
|
||||
expectedStrings := map[string]interface{}{"test-role1": true, "test-role2": true}
|
||||
expectStrings(t, respListRole.Data["keys"].([]string), expectedStrings)
|
||||
|
||||
// delete test-role2
|
||||
c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/role/test-role2",
|
||||
Operation: logical.DeleteOperation,
|
||||
Storage: storage,
|
||||
})
|
||||
|
||||
// list roles again and validate response
|
||||
respListRoleAfterDelete, listErrAfterDelete := c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/role",
|
||||
Operation: logical.ListOperation,
|
||||
Storage: storage,
|
||||
})
|
||||
expectSuccess(t, respListRoleAfterDelete, listErrAfterDelete)
|
||||
|
||||
// validate list response
|
||||
delete(expectedStrings, "test-role2")
|
||||
expectStrings(t, respListRoleAfterDelete.Data["keys"].([]string), expectedStrings)
|
||||
}
|
||||
|
||||
// TestOIDC_Path_OIDCKeyKey tests CRUD operations for keys
|
||||
func TestOIDC_Path_OIDCKeyKey(t *testing.T) {
|
||||
c, _, _ := TestCoreUnsealed(t)
|
||||
ctx := namespace.RootContext(nil)
|
||||
storage := &logical.InmemStorage{}
|
||||
|
||||
// Create a test key "test-key" -- should succeed
|
||||
resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/key/test-key",
|
||||
Operation: logical.CreateOperation,
|
||||
Storage: storage,
|
||||
})
|
||||
expectSuccess(t, resp, err)
|
||||
|
||||
// Read "test-key"
|
||||
respReadTestKey, err1 := c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/key/test-key",
|
||||
Operation: logical.ReadOperation,
|
||||
Storage: storage,
|
||||
})
|
||||
expectSuccess(t, respReadTestKey, err1)
|
||||
|
||||
// Update "test-key" -- should succeed
|
||||
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/key/test-key",
|
||||
Operation: logical.UpdateOperation,
|
||||
Data: map[string]interface{}{
|
||||
"rotation_period": "10m",
|
||||
"verification_ttl": "1h",
|
||||
},
|
||||
Storage: storage,
|
||||
})
|
||||
expectSuccess(t, resp, err)
|
||||
|
||||
// Read "test-key" again
|
||||
respReadTestKeyAfterUpdate, err2 := c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/key/test-key",
|
||||
Operation: logical.ReadOperation,
|
||||
Storage: storage,
|
||||
})
|
||||
expectSuccess(t, respReadTestKeyAfterUpdate, err2)
|
||||
|
||||
// Compare response for "test-key" before and after it was updated
|
||||
expectedDiff := map[string]interface{}{
|
||||
"Data.map[rotation_period]: 86400 != 600": true, // from 24h to 10m
|
||||
"Data.map[verification_ttl]: 86400 != 3600": true, // from 24h to 1h
|
||||
}
|
||||
diff := deep.Equal(respReadTestKey, respReadTestKeyAfterUpdate)
|
||||
expectStrings(t, diff, expectedDiff)
|
||||
|
||||
// Create a role that depends on test key
|
||||
c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/role/test-role",
|
||||
Operation: logical.UpdateOperation,
|
||||
Data: map[string]interface{}{
|
||||
"key": "test-key",
|
||||
},
|
||||
Storage: storage,
|
||||
})
|
||||
|
||||
// Delete test-key -- should fail because test-role depends on test-key
|
||||
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/key/test-key",
|
||||
Operation: logical.DeleteOperation,
|
||||
Storage: storage,
|
||||
})
|
||||
expectError(t, resp, err)
|
||||
// validate error message
|
||||
expectedStrings := map[string]interface{}{
|
||||
"unable to delete key \"test-key\" because it is currently referenced by these roles: test-role": true,
|
||||
}
|
||||
expectStrings(t, []string{resp.Data["error"].(string)}, expectedStrings)
|
||||
|
||||
// Delete test-role
|
||||
c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/role/test-role",
|
||||
Operation: logical.DeleteOperation,
|
||||
Storage: storage,
|
||||
})
|
||||
|
||||
// Delete test-key -- should succeed this time because no roles depend on test-key
|
||||
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/key/test-key",
|
||||
Operation: logical.DeleteOperation,
|
||||
Storage: storage,
|
||||
})
|
||||
expectSuccess(t, resp, err)
|
||||
}
|
||||
|
||||
// TestOIDC_Path_OIDCKey tests the List operation for keys
|
||||
func TestOIDC_Path_OIDCKey(t *testing.T) {
|
||||
c, _, _ := TestCoreUnsealed(t)
|
||||
ctx := namespace.RootContext(nil)
|
||||
storage := &logical.InmemStorage{}
|
||||
|
||||
// Prepare two keys, test-key1 and test-key2
|
||||
c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/key/test-key1",
|
||||
Operation: logical.CreateOperation,
|
||||
Storage: storage,
|
||||
})
|
||||
|
||||
c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/key/test-key2",
|
||||
Operation: logical.CreateOperation,
|
||||
Storage: storage,
|
||||
})
|
||||
|
||||
// list keys
|
||||
respListKey, listErr := c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/key",
|
||||
Operation: logical.ListOperation,
|
||||
Storage: storage,
|
||||
})
|
||||
expectSuccess(t, respListKey, listErr)
|
||||
|
||||
// validate list response
|
||||
expectedStrings := map[string]interface{}{"test-key1": true, "test-key2": true}
|
||||
expectStrings(t, respListKey.Data["keys"].([]string), expectedStrings)
|
||||
|
||||
// delete test-key2
|
||||
c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/key/test-key2",
|
||||
Operation: logical.DeleteOperation,
|
||||
Storage: storage,
|
||||
})
|
||||
|
||||
// list keyes again and validate response
|
||||
respListKeyAfterDelete, listErrAfterDelete := c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/key",
|
||||
Operation: logical.ListOperation,
|
||||
Storage: storage,
|
||||
})
|
||||
expectSuccess(t, respListKeyAfterDelete, listErrAfterDelete)
|
||||
|
||||
// validate list response
|
||||
delete(expectedStrings, "test-key2")
|
||||
expectStrings(t, respListKeyAfterDelete.Data["keys"].([]string), expectedStrings)
|
||||
}
|
||||
|
||||
// TestOIDC_PublicKeys tests that public keys are updated by
|
||||
// key creation, rotation, and deletion
|
||||
func TestOIDC_PublicKeys(t *testing.T) {
|
||||
c, _, _ := TestCoreUnsealed(t)
|
||||
ctx := namespace.RootContext(nil)
|
||||
storage := &logical.InmemStorage{}
|
||||
|
||||
// Create a test key "test-key"
|
||||
c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/key/test-key",
|
||||
Operation: logical.CreateOperation,
|
||||
Storage: storage,
|
||||
})
|
||||
|
||||
// .well-known/keys should contain 1 public key
|
||||
resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/.well-known/keys",
|
||||
Operation: logical.ReadOperation,
|
||||
Storage: storage,
|
||||
})
|
||||
expectSuccess(t, resp, err)
|
||||
// parse response
|
||||
responseJWKS := &jose.JSONWebKeySet{}
|
||||
json.Unmarshal(resp.Data["http_raw_body"].([]byte), responseJWKS)
|
||||
if len(responseJWKS.Keys) != 1 {
|
||||
t.Fatalf("expected 1 public key but instead got %d", len(responseJWKS.Keys))
|
||||
}
|
||||
|
||||
// rotate test-key a few times, each rotate should increase the length of public keys returned
|
||||
// by the .well-known endpoint
|
||||
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/key/test-key/rotate",
|
||||
Operation: logical.UpdateOperation,
|
||||
Storage: storage,
|
||||
})
|
||||
expectSuccess(t, resp, err)
|
||||
c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/key/test-key/rotate",
|
||||
Operation: logical.UpdateOperation,
|
||||
Storage: storage,
|
||||
})
|
||||
|
||||
// .well-known/keys should contain 3 public keys
|
||||
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/.well-known/keys",
|
||||
Operation: logical.ReadOperation,
|
||||
Storage: storage,
|
||||
})
|
||||
expectSuccess(t, resp, err)
|
||||
// parse response
|
||||
json.Unmarshal(resp.Data["http_raw_body"].([]byte), responseJWKS)
|
||||
if len(responseJWKS.Keys) != 3 {
|
||||
t.Fatalf("expected 3 public keya but instead got %d", len(responseJWKS.Keys))
|
||||
}
|
||||
|
||||
// create another named key
|
||||
c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/key/test-key2",
|
||||
Operation: logical.CreateOperation,
|
||||
Storage: storage,
|
||||
})
|
||||
|
||||
// delete test key
|
||||
c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/key/test-key",
|
||||
Operation: logical.DeleteOperation,
|
||||
Storage: storage,
|
||||
})
|
||||
|
||||
// .well-known/keys should contain 1 public key, all of the public keys
|
||||
// from named key "test-key" should have been deleted
|
||||
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/.well-known/keys",
|
||||
Operation: logical.ReadOperation,
|
||||
Storage: storage,
|
||||
})
|
||||
expectSuccess(t, resp, err)
|
||||
// parse response
|
||||
json.Unmarshal(resp.Data["http_raw_body"].([]byte), responseJWKS)
|
||||
if len(responseJWKS.Keys) != 1 {
|
||||
t.Fatalf("expected 1 public keya but instead got %d", len(responseJWKS.Keys))
|
||||
}
|
||||
}
|
||||
|
||||
// TestOIDC_SignIDToken tests acquiring a signed token and verifying the public portion
|
||||
// of the signing key
|
||||
func TestOIDC_SignIDToken(t *testing.T) {
|
||||
c, _, _ := TestCoreUnsealed(t)
|
||||
ctx := namespace.RootContext(nil)
|
||||
storage := &logical.InmemStorage{}
|
||||
|
||||
// Create and load an entity, an entity is required to generate an ID token
|
||||
testEntity := &identity.Entity{
|
||||
Name: "test-entity-name",
|
||||
ID: "test-entity-id",
|
||||
BucketKey: "test-entity-bucket-key",
|
||||
}
|
||||
|
||||
txn := c.identityStore.db.Txn(true)
|
||||
defer txn.Abort()
|
||||
err := c.identityStore.upsertEntityInTxn(ctx, txn, testEntity, nil, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
txn.Commit()
|
||||
|
||||
// Create a test key "test-key"
|
||||
c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/key/test-key",
|
||||
Operation: logical.CreateOperation,
|
||||
Storage: storage,
|
||||
})
|
||||
|
||||
// Create a test role "test-role"
|
||||
c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/role/test-role",
|
||||
Operation: logical.CreateOperation,
|
||||
Data: map[string]interface{}{
|
||||
"key": "test-key",
|
||||
},
|
||||
Storage: storage,
|
||||
})
|
||||
|
||||
// Generate a token against the role "test-role" -- should succeed
|
||||
resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/token/test-role",
|
||||
Operation: logical.ReadOperation,
|
||||
Storage: storage,
|
||||
EntityID: "test-entity-id",
|
||||
})
|
||||
expectSuccess(t, resp, err)
|
||||
parsedToken, err := jwt.ParseSigned(resp.Data["token"].(string))
|
||||
if err != nil {
|
||||
t.Fatalf("error parsing token: %s", err.Error())
|
||||
}
|
||||
|
||||
// Acquire the public parts of the key that signed parsedToken
|
||||
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/.well-known/keys",
|
||||
Operation: logical.ReadOperation,
|
||||
Storage: storage,
|
||||
})
|
||||
responseJWKS := &jose.JSONWebKeySet{}
|
||||
json.Unmarshal(resp.Data["http_raw_body"].([]byte), responseJWKS)
|
||||
|
||||
// Validate the signature
|
||||
claims := &jwt.Claims{}
|
||||
if err := parsedToken.Claims(responseJWKS.Keys[0], claims); err != nil {
|
||||
t.Fatalf("unable to validate signed token, err:\n%#v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOIDC_PeriodicFunc tests timing logic for running key
|
||||
// rotations and expiration actions.
|
||||
func TestOIDC_PeriodicFunc(t *testing.T) {
|
||||
// Prepare a storage to run through periodicFunc
|
||||
c, _, _ := TestCoreUnsealed(t)
|
||||
ctx := namespace.RootContext(nil)
|
||||
storage := &logical.InmemStorage{}
|
||||
|
||||
// populate storage with a named key
|
||||
period := 2 * time.Second
|
||||
keyName := "test-key"
|
||||
key, _ := rsa.GenerateKey(rand.Reader, 2048)
|
||||
id, _ := uuid.GenerateUUID()
|
||||
jwk := &jose.JSONWebKey{
|
||||
Key: key,
|
||||
KeyID: id,
|
||||
Algorithm: "RS256",
|
||||
Use: "sig",
|
||||
}
|
||||
namedKey := &namedKey{
|
||||
Name: keyName,
|
||||
Algorithm: "RS256",
|
||||
VerificationTTL: 1 * period,
|
||||
RotationPeriod: 1 * period,
|
||||
KeyRing: nil,
|
||||
SigningKey: jwk,
|
||||
NextRotation: time.Now().Add(1 * time.Second),
|
||||
}
|
||||
|
||||
// Store namedKey
|
||||
entry, _ := logical.StorageEntryJSON(namedKeyConfigPath+keyName, namedKey)
|
||||
if err := storage.Put(ctx, entry); err != nil {
|
||||
t.Fatalf("writing to in mem storage failed")
|
||||
}
|
||||
|
||||
// Time 0 - 1 Period
|
||||
// PeriodicFunc should set nextRun - nothing else
|
||||
c.identityStore.oidcPeriodicFunc(ctx, storage)
|
||||
entry, _ = storage.Get(ctx, namedKeyConfigPath+keyName)
|
||||
entry.DecodeJSON(&namedKey)
|
||||
if len(namedKey.KeyRing) != 0 {
|
||||
t.Fatalf("expected namedKey's KeyRing to be of length 0 but was: %#v", len(namedKey.KeyRing))
|
||||
}
|
||||
// There should be no public keys yet
|
||||
publicKeys, _ := storage.List(ctx, publicKeysConfigPath)
|
||||
if len(publicKeys) != 0 {
|
||||
t.Fatalf("expected publicKeys to be of length 0 but was: %#v", len(publicKeys))
|
||||
}
|
||||
// Next run should be set
|
||||
v, _ := c.identityStore.oidcCache.Get("nextRun")
|
||||
if v == nil {
|
||||
t.Fatalf("Expected nextRun to be set but it was nil")
|
||||
}
|
||||
earlierNextRun := v.(time.Time)
|
||||
|
||||
// Time 1 - 2 Period
|
||||
// PeriodicFunc should rotate namedKey and update nextRun
|
||||
time.Sleep(period)
|
||||
c.identityStore.oidcPeriodicFunc(ctx, storage)
|
||||
entry, _ = storage.Get(ctx, namedKeyConfigPath+keyName)
|
||||
entry.DecodeJSON(&namedKey)
|
||||
if len(namedKey.KeyRing) != 1 {
|
||||
t.Fatalf("expected namedKey's KeyRing to be of length 1 but was: %#v", len(namedKey.KeyRing))
|
||||
}
|
||||
// There should be one public key
|
||||
publicKeys, _ = storage.List(ctx, publicKeysConfigPath)
|
||||
if len(publicKeys) != 1 {
|
||||
t.Fatalf("expected publicKeys to be of length 1 but was: %#v", len(publicKeys))
|
||||
}
|
||||
// nextRun should have been updated
|
||||
v, _ = c.identityStore.oidcCache.Get("nextRun")
|
||||
laterNextRun := v.(time.Time)
|
||||
if !laterNextRun.After(earlierNextRun) {
|
||||
t.Fatalf("laterNextRun: %#v is not after earlierNextRun: %#v", laterNextRun.String(), earlierNextRun.String())
|
||||
}
|
||||
|
||||
// Time 2-3
|
||||
// PeriodicFunc should rotate namedKey and expire 1 public key
|
||||
time.Sleep(period)
|
||||
c.identityStore.oidcPeriodicFunc(ctx, storage)
|
||||
entry, _ = storage.Get(ctx, namedKeyConfigPath+keyName)
|
||||
entry.DecodeJSON(&namedKey)
|
||||
if len(namedKey.KeyRing) != 2 {
|
||||
t.Fatalf("expected namedKey's KeyRing to be of length 2 but was: %#v", len(namedKey.KeyRing))
|
||||
}
|
||||
// There should be two public keys
|
||||
publicKeys, _ = storage.List(ctx, publicKeysConfigPath)
|
||||
if len(publicKeys) != 2 {
|
||||
t.Fatalf("expected publicKeys to be of length 2 but was: %#v", len(publicKeys))
|
||||
}
|
||||
|
||||
// Time 3-4
|
||||
// PeriodicFunc should rotate namedKey and expire 1 public key
|
||||
time.Sleep(period)
|
||||
c.identityStore.oidcPeriodicFunc(ctx, storage)
|
||||
entry, _ = storage.Get(ctx, namedKeyConfigPath+keyName)
|
||||
entry.DecodeJSON(&namedKey)
|
||||
if len(namedKey.KeyRing) != 2 {
|
||||
t.Fatalf("expected namedKey's KeyRing to be of length 1 but was: %#v", len(namedKey.KeyRing))
|
||||
}
|
||||
// There should be two public keys
|
||||
publicKeys, _ = storage.List(ctx, publicKeysConfigPath)
|
||||
if len(publicKeys) != 2 {
|
||||
t.Fatalf("expected publicKeys to be of length 1 but was: %#v", len(publicKeys))
|
||||
}
|
||||
}
|
||||
|
||||
// some helpers
|
||||
func expectSuccess(t *testing.T, resp *logical.Response, err error) {
|
||||
if err != nil || (resp != nil && resp.IsError()) {
|
||||
t.Fatalf("expected success but got error:\n%v\nresp: %#v", err, resp)
|
||||
}
|
||||
}
|
||||
|
||||
func expectError(t *testing.T, resp *logical.Response, err error) {
|
||||
if err == nil {
|
||||
if resp == nil || !resp.IsError() {
|
||||
t.Fatalf("expected error but got success; error:\n%v\nresp: %#v", err, resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// expectString fails unless every string in actualStrings is also included in expectedStrings and
|
||||
// the length of actualStrings and expectedStrings are the same
|
||||
func expectStrings(t *testing.T, actualStrings []string, expectedStrings map[string]interface{}) {
|
||||
if len(actualStrings) != len(expectedStrings) {
|
||||
t.Fatalf("expectStrings mismatch:\nactual strings:\n%#v\nexpected strings:\n%#v\n", actualStrings, expectedStrings)
|
||||
}
|
||||
for _, actualString := range actualStrings {
|
||||
_, ok := expectedStrings[actualString]
|
||||
if !ok {
|
||||
t.Fatalf("the string %q was not expected", actualString)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,6 +4,8 @@ import (
|
|||
"regexp"
|
||||
"sync"
|
||||
|
||||
"github.com/patrickmn/go-cache"
|
||||
|
||||
log "github.com/hashicorp/go-hclog"
|
||||
memdb "github.com/hashicorp/go-memdb"
|
||||
"github.com/hashicorp/vault/helper/identity"
|
||||
|
@ -57,6 +59,11 @@ type IdentityStore struct {
|
|||
// groupLock is used to protect modifications to group entries
|
||||
groupLock sync.RWMutex
|
||||
|
||||
// oidcCache stores common response data as well as when the periodic func needs
|
||||
// to run. This is conservatively managed, and most writes to the OIDC endpoints
|
||||
// will invalidate the cache.
|
||||
oidcCache *cache.Cache
|
||||
|
||||
// logger is the server logger copied over from core
|
||||
logger log.Logger
|
||||
|
||||
|
|
|
@ -281,7 +281,8 @@ func parsePaths(result *Policy, list *ast.ObjectList, performTemplating bool, en
|
|||
|
||||
// Check the path
|
||||
if performTemplating {
|
||||
_, templated, err := identity.PopulateString(&identity.PopulateStringInput{
|
||||
_, templated, err := identity.PopulateString(identity.PopulateStringInput{
|
||||
Mode: identity.ACLTemplating,
|
||||
String: key,
|
||||
Entity: entity,
|
||||
Groups: groups,
|
||||
|
@ -292,7 +293,8 @@ func parsePaths(result *Policy, list *ast.ObjectList, performTemplating bool, en
|
|||
}
|
||||
key = templated
|
||||
} else {
|
||||
hasTemplating, _, err := identity.PopulateString(&identity.PopulateStringInput{
|
||||
hasTemplating, _, err := identity.PopulateString(identity.PopulateStringInput{
|
||||
Mode: identity.ACLTemplating,
|
||||
ValidityCheckOnly: true,
|
||||
String: key,
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue