Skip to content

Improve performance for fetching authorized entries #6034

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
May 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion pkg/agent/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
svidv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/svid/v1"
"github.com/spiffe/spire-api-sdk/proto/spire/api/types"
"github.com/spiffe/spire/pkg/common/telemetry"
"github.com/spiffe/spire/pkg/server/api"
"github.com/spiffe/spire/pkg/server/api/entry/v1"
"github.com/spiffe/spire/proto/spire/common"
"github.com/spiffe/spire/test/spiretest"
Expand Down Expand Up @@ -1023,7 +1024,13 @@ func (c *fakeEntryServer) GetAuthorizedEntries(_ context.Context, in *entryv1.Ge

func (c *fakeEntryServer) SyncAuthorizedEntries(stream entryv1.Entry_SyncAuthorizedEntriesServer) error {
const entryPageSize = 2
return entry.SyncAuthorizedEntries(stream, c.entries, entryPageSize)

entries := []api.ReadOnlyEntry{}
for _, entry := range c.entries {
entries = append(entries, api.NewReadOnlyEntry(entry))
}

return entry.SyncAuthorizedEntries(stream, entries, entryPageSize)
}

type fakeBundleServer struct {
Expand Down
4 changes: 2 additions & 2 deletions pkg/server/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ import (
type AuthorizedEntryFetcher interface {
// LookupAuthorizedEntries fetches the entries in entryIDs that the
// specified SPIFFE ID is authorized for
LookupAuthorizedEntries(ctx context.Context, id spiffeid.ID, entryIDs map[string]struct{}) (map[string]*types.Entry, error)
LookupAuthorizedEntries(ctx context.Context, id spiffeid.ID, entryIDs map[string]struct{}) (map[string]ReadOnlyEntry, error)
// FetchAuthorizedEntries fetches the entries that the specified
// SPIFFE ID is authorized for
FetchAuthorizedEntries(ctx context.Context, id spiffeid.ID) ([]*types.Entry, error)
FetchAuthorizedEntries(ctx context.Context, id spiffeid.ID) ([]ReadOnlyEntry, error)
}

// AttestedNodeToProto converts an agent from the given *common.AttestedNode with
Expand Down
118 changes: 118 additions & 0 deletions pkg/server/api/entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,130 @@ import (
"github.com/spiffe/spire/pkg/common/protoutil"
"github.com/spiffe/spire/pkg/common/x509util"
"github.com/spiffe/spire/proto/spire/common"
"google.golang.org/protobuf/proto"
)

const (
hintMaximumLength = 1024
)

type ReadOnlyEntry struct {
entry *types.Entry
}

func NewReadOnlyEntry(entry *types.Entry) ReadOnlyEntry {
return ReadOnlyEntry{
entry: entry,
}
}

func (e ReadOnlyEntry) GetId() string {
return e.entry.Id
}

func (e *ReadOnlyEntry) GetSpiffeId() *types.SPIFFEID {
return &types.SPIFFEID{
TrustDomain: e.entry.SpiffeId.TrustDomain,
Path: e.entry.SpiffeId.Path,
}
}

func (e *ReadOnlyEntry) GetX509SvidTtl() int32 {
return e.entry.X509SvidTtl
}

func (e *ReadOnlyEntry) GetJwtSvidTtl() int32 {
return e.entry.JwtSvidTtl
}

func (e *ReadOnlyEntry) GetDnsNames() []string {
return slices.Clone(e.entry.DnsNames)
}

func (e *ReadOnlyEntry) GetRevisionNumber() int64 {
return e.entry.RevisionNumber
}

func (e *ReadOnlyEntry) GetCreatedAt() int64 {
return e.entry.CreatedAt
}
Comment on lines +42 to +60
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't seem like we there is test coverage for this, could you add that?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a test to verify that the getters return the expected value


// Manually clone the entry instead of using the protobuf helpers
// since those are two times slower.
func (e *ReadOnlyEntry) Clone(mask *types.EntryMask) *types.Entry {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit concerned about the potential for field skew here. If the types.Entry struct is updated in the future (e.g., new fields are added), the manual Clone implementation will also need to be updated. Otherwise, newly added fields won't be cloned, which could lead to subtle, hard-to-detect bugs.

It might be helpful to add a comment warning about this, or ideally, include a test (perhaps using reflection) that ensures all fields are properly cloned.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a test for this, which indeed uses reflection to make sure all fields are cloned:

func TestReadOnlyEntryClone(t *testing.T) {

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I missed that, thanks!

if mask == nil {
return proto.Clone(e.entry).(*types.Entry)
}

clone := &types.Entry{}
clone.Id = e.entry.Id
if mask.SpiffeId {
clone.SpiffeId = e.GetSpiffeId()
}

if mask.ParentId {
clone.ParentId = &types.SPIFFEID{
TrustDomain: e.entry.ParentId.TrustDomain,
Path: e.entry.ParentId.Path,
}
}

if mask.Selectors {
for _, selector := range e.entry.Selectors {
clone.Selectors = append(clone.Selectors, &types.Selector{
Type: selector.Type,
Value: selector.Value,
})
}
}

if mask.FederatesWith {
clone.FederatesWith = slices.Clone(e.entry.FederatesWith)
}

if mask.Admin {
clone.Admin = e.entry.Admin
}

if mask.Downstream {
clone.Downstream = e.entry.Admin
}

if mask.ExpiresAt {
clone.ExpiresAt = e.entry.ExpiresAt
}

if mask.DnsNames {
clone.DnsNames = slices.Clone(e.entry.DnsNames)
}

if mask.RevisionNumber {
clone.RevisionNumber = e.entry.RevisionNumber
}

if mask.StoreSvid {
clone.StoreSvid = e.entry.StoreSvid
}

if mask.X509SvidTtl {
clone.X509SvidTtl = e.entry.X509SvidTtl
}

if mask.JwtSvidTtl {
clone.JwtSvidTtl = e.entry.JwtSvidTtl
}

if mask.Hint {
clone.Hint = e.entry.Hint
}

if mask.CreatedAt {
clone.CreatedAt = e.entry.CreatedAt
}

return clone
}

// RegistrationEntriesToProto converts RegistrationEntry's into Entry's
func RegistrationEntriesToProto(es []*common.RegistrationEntry) ([]*types.Entry, error) {
if es == nil {
Expand Down
42 changes: 20 additions & 22 deletions pkg/server/api/entry/v1/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -390,14 +390,13 @@ func (s *Service) GetAuthorizedEntries(ctx context.Context, req *entryv1.GetAuth
if err != nil {
return nil, err
}
for i, entry := range entries {
applyMask(entry, req.OutputMask)
entries[i] = entry
}

resp := &entryv1.GetAuthorizedEntriesResponse{
Entries: entries,
resp := &entryv1.GetAuthorizedEntriesResponse{}

for _, entry := range entries {
resp.Entries = append(resp.Entries, entry.Clone(req.OutputMask))
}

rpccontext.AuditRPC(ctx)

return resp, nil
Expand All @@ -423,7 +422,7 @@ func (s *Service) SyncAuthorizedEntries(stream entryv1.Entry_SyncAuthorizedEntri
return SyncAuthorizedEntries(stream, entries, s.entryPageSize)
}

func SyncAuthorizedEntries(stream entryv1.Entry_SyncAuthorizedEntriesServer, entries []*types.Entry, entryPageSize int) (err error) {
func SyncAuthorizedEntries(stream entryv1.Entry_SyncAuthorizedEntriesServer, entries []api.ReadOnlyEntry, entryPageSize int) (err error) {
// Receive the initial request with the output mask.
req, err := stream.Recv()
if err != nil {
Expand All @@ -446,18 +445,17 @@ func SyncAuthorizedEntries(stream entryv1.Entry_SyncAuthorizedEntriesServer, ent

// Apply output mask to entries. The output mask field will be
// intentionally ignored on subsequent requests.
for i, entry := range entries {
applyMask(entry, req.OutputMask)
entries[i] = entry
}
initialOutputMask := req.OutputMask

// If the number of entries is less than or equal to the entry page size,
// then just send the full list back. Otherwise, we'll send a sparse list
// and then stream back full entries as requested.
if len(entries) <= entryPageSize {
return stream.Send(&entryv1.SyncAuthorizedEntriesResponse{
Entries: entries,
})
resp := &entryv1.SyncAuthorizedEntriesResponse{}
for _, entry := range entries {
resp.Entries = append(resp.Entries, entry.Clone(initialOutputMask))
}
return stream.Send(resp)
}

// Prepopulate the entry page used in the response with empty entry structs.
Expand All @@ -474,9 +472,9 @@ func SyncAuthorizedEntries(stream entryv1.Entry_SyncAuthorizedEntriesServer, ent
more = true
}
for j, entry := range entries[i : i+n] {
entryRevisions[j].Id = entry.Id
entryRevisions[j].RevisionNumber = entry.RevisionNumber
entryRevisions[j].CreatedAt = entry.CreatedAt
entryRevisions[j].Id = entry.GetId()
entryRevisions[j].RevisionNumber = entry.GetRevisionNumber()
entryRevisions[j].CreatedAt = entry.GetCreatedAt()
}

if err := stream.Send(&entryv1.SyncAuthorizedEntriesResponse{
Expand Down Expand Up @@ -529,7 +527,7 @@ func SyncAuthorizedEntries(stream entryv1.Entry_SyncAuthorizedEntriesServer, ent
entriesToSearch := entries
for _, id := range req.Ids {
i, found := sort.Find(len(entriesToSearch), func(i int) int {
return strings.Compare(id, entriesToSearch[i].Id)
return strings.Compare(id, entriesToSearch[i].GetId())
})
if found {
if len(resp.Entries) == entryPageSize {
Expand All @@ -542,7 +540,7 @@ func SyncAuthorizedEntries(stream entryv1.Entry_SyncAuthorizedEntriesServer, ent
}
resp.Entries = resp.Entries[:0]
}
resp.Entries = append(resp.Entries, entriesToSearch[i])
resp.Entries = append(resp.Entries, entriesToSearch[i].Clone(initialOutputMask))
}
entriesToSearch = entriesToSearch[i:]
if len(entriesToSearch) == 0 {
Expand All @@ -559,7 +557,7 @@ func SyncAuthorizedEntries(stream entryv1.Entry_SyncAuthorizedEntriesServer, ent
}

// fetchEntries fetches authorized entries using caller ID from context
func (s *Service) fetchEntries(ctx context.Context, log logrus.FieldLogger) ([]*types.Entry, error) {
func (s *Service) fetchEntries(ctx context.Context, log logrus.FieldLogger) ([]api.ReadOnlyEntry, error) {
callerID, ok := rpccontext.CallerID(ctx)
if !ok {
return nil, api.MakeErr(log, codes.Internal, "caller ID missing from request context", nil)
Expand Down Expand Up @@ -843,8 +841,8 @@ func fieldsFromCountEntryFilter(ctx context.Context, td spiffeid.TrustDomain, fi
return fields
}

func sortEntriesByID(entries []*types.Entry) {
func sortEntriesByID(entries []api.ReadOnlyEntry) {
sort.Slice(entries, func(a, b int) bool {
return entries[a].Id < entries[b].Id
return entries[a].GetId() < entries[b].GetId()
})
}
13 changes: 9 additions & 4 deletions pkg/server/api/entry/v1/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4841,21 +4841,21 @@ type entryFetcher struct {
entries []*types.Entry
}

func (f *entryFetcher) LookupAuthorizedEntries(ctx context.Context, agentID spiffeid.ID, _ map[string]struct{}) (map[string]*types.Entry, error) {
func (f *entryFetcher) LookupAuthorizedEntries(ctx context.Context, agentID spiffeid.ID, _ map[string]struct{}) (map[string]api.ReadOnlyEntry, error) {
entries, err := f.FetchAuthorizedEntries(ctx, agentID)
if err != nil {
return nil, err
}

entriesMap := make(map[string]*types.Entry)
entriesMap := make(map[string]api.ReadOnlyEntry)
for _, entry := range entries {
entriesMap[entry.GetId()] = entry
}

return entriesMap, nil
}

func (f *entryFetcher) FetchAuthorizedEntries(ctx context.Context, agentID spiffeid.ID) ([]*types.Entry, error) {
func (f *entryFetcher) FetchAuthorizedEntries(ctx context.Context, agentID spiffeid.ID) ([]api.ReadOnlyEntry, error) {
if f.err != "" {
return nil, status.Error(codes.Internal, f.err)
}
Expand All @@ -4869,7 +4869,12 @@ func (f *entryFetcher) FetchAuthorizedEntries(ctx context.Context, agentID spiff
return nil, fmt.Errorf("provided caller id is different to expected")
}

return f.entries, nil
entries := []api.ReadOnlyEntry{}
for _, entry := range f.entries {
entries = append(entries, api.NewReadOnlyEntry(entry))
}

return entries, nil
}

type HasID interface {
Expand Down
Loading
Loading