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:
Jim Kalafut 2019-06-21 10:23:39 -07:00 committed by Lexman
parent 5d0c68ca74
commit 2bf5db4fe8
7 changed files with 2463 additions and 45 deletions

View File

@ -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

View File

@ -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)
}
}

View File

@ -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()
}
}

1344
vault/identity_store_oidc.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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)
}
}
}

View File

@ -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

View File

@ -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,
})