add ce side code and stubs for rotation manager

* add ce side code and stubs

* add changelog

* style refactor

* try to use APIPath as mount point instead of request field

* fix linter

* return a response struct instead of a pure timestamp

* add issue time to response

* add ttl to GetRotationInformation response

* rename field for clarity

* update ttl to just seconds

* rename next and last rotation time field; describe what they are

* rename function

* catch up to ent PR

* fix patch merge mistake
This commit is contained in:
kpcraig 2025-07-15 12:48:00 -04:00 committed by GitHub
parent 720bd68c7d
commit 8f522a2bca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 627 additions and 331 deletions

3
changelog/31053.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
sdk: add stub code for retrieving rotation schedule information
```

View File

@ -87,13 +87,19 @@ func (p *AutomatedRotationParams) PopulateAutomatedRotationData(m map[string]int
}
func (p *AutomatedRotationParams) ShouldRegisterRotationJob() bool {
return p.RotationSchedule != "" || p.RotationPeriod != 0
return p.HasNonzeroRotationValues()
}
func (p *AutomatedRotationParams) ShouldDeregisterRotationJob() bool {
return p.DisableAutomatedRotation || (p.RotationSchedule == "" && p.RotationPeriod == 0)
}
// HasNonzeroRotationValues returns true if either of the primary rotation values (RotationSchedule or RotationPeriod)
// are not the zero value.
func (p *AutomatedRotationParams) HasNonzeroRotationValues() bool {
return p.RotationSchedule != "" || p.RotationPeriod != 0
}
// AddAutomatedRotationFields adds plugin identity token fields to the given
// field schema map.
func AddAutomatedRotationFields(m map[string]*framework.FieldSchema) {

View File

@ -102,6 +102,9 @@ type SystemView interface {
// GenerateIdentityToken returns an identity token for the requesting plugin.
GenerateIdentityToken(ctx context.Context, req *pluginutil.IdentityTokenRequest) (*pluginutil.IdentityTokenResponse, error)
// GetRotationInformation gets rotation information from the system about an established rotation job.
GetRotationInformation(ctx context.Context, req *rotation.RotationInfoRequest) (*rotation.RotationInfoResponse, error)
// RegisterRotationJob returns a rotation ID after registering a
// rotation job for the requesting plugin.
// NOTE: This method is intended for use only by HashiCorp Vault Enterprise plugins.
@ -301,6 +304,10 @@ func (d StaticSystemView) APILockShouldBlockRequest() (bool, error) {
return d.APILockShouldBlockRequestVal, nil
}
func (d StaticSystemView) GetRotationInformation(ctx context.Context, req *rotation.RotationInfoRequest) (*rotation.RotationInfoResponse, error) {
return nil, errors.New("GetRotationInformation is not implemented in StaticSystemView")
}
func (d StaticSystemView) RegisterRotationJob(_ context.Context, _ *rotation.RotationJobConfigureRequest) (rotationID string, err error) {
return "", errors.New("RegisterRotationJob is not implemented in StaticSystemView")
}

View File

@ -227,6 +227,21 @@ func (s *gRPCSystemViewClient) GenerateIdentityToken(ctx context.Context, req *p
}, nil
}
func (s *gRPCSystemViewClient) GetRotationInformation(ctx context.Context, req *rotation.RotationInfoRequest) (*rotation.RotationInfoResponse, error) {
resp, err := s.client.GetRotationInformation(ctx, &pb.RotationInfoRequest{
MountPath: req.ReqPath,
})
if err != nil {
return nil, err
}
return &rotation.RotationInfoResponse{
NextVaultRotation: time.Unix(resp.ExpireTime, 0),
LastVaultRotation: time.Unix(resp.IssueTime, 0),
TTL: resp.TTL,
}, nil
}
func (s *gRPCSystemViewClient) RegisterRotationJob(ctx context.Context, req *rotation.RotationJobConfigureRequest) (id string, retErr error) {
cfgReq := &pb.RegisterRotationJobRequest{
Job: &pb.RotationJobInput{
@ -464,6 +479,27 @@ func (s *gRPCSystemViewServer) GenerateIdentityToken(ctx context.Context, req *p
}, nil
}
func (s *gRPCSystemViewServer) GetRotationInformation(ctx context.Context, req *pb.RotationInfoRequest) (*pb.RotationInfoReply, error) {
if s.impl == nil {
return nil, errMissingSystemView
}
cfgReq := &rotation.RotationInfoRequest{
ReqPath: req.MountPath,
}
resp, err := s.impl.GetRotationInformation(ctx, cfgReq)
if err != nil {
return &pb.RotationInfoReply{}, status.Error(codes.Internal, err.Error())
}
return &pb.RotationInfoReply{
IssueTime: resp.LastVaultRotation.Unix(),
ExpireTime: resp.NextVaultRotation.Unix(),
TTL: resp.TTL,
}, nil
}
func (s *gRPCSystemViewServer) RegisterRotationJob(ctx context.Context, req *pb.RegisterRotationJobRequest) (*pb.RegisterRotationJobResponse, error) {
if s.impl == nil {
return nil, errMissingSystemView

View File

@ -5,6 +5,7 @@ package mock
import (
"context"
"errors"
"fmt"
"os"
"testing"
@ -137,6 +138,16 @@ func expectInternalValue(t *testing.T, client *api.Client, mountPath, expected s
func (b *backend) rotateRootCredential(ctx context.Context, req *logical.Request) error {
b.Logger().Debug("mock rotateRootCredential")
cfg, err := b.configEntry(ctx, req.Storage)
if err != nil {
return err
}
if cfg.FailRotate {
return errors.New("mock plugin was asked to fail to rotate")
}
b.internal = "rotated"
return nil
}

View File

@ -16,7 +16,11 @@ import (
func pathConfig(b *backend) *framework.Path {
p := &framework.Path{
Pattern: "config",
Fields: map[string]*framework.FieldSchema{},
Fields: map[string]*framework.FieldSchema{
"fail_rotate": {
Type: framework.TypeBool,
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.CreateOperation: b.pathConfigUpdate,
logical.UpdateOperation: b.pathConfigUpdate,
@ -37,6 +41,10 @@ func (b *backend) pathConfigUpdate(ctx context.Context, req *logical.Request, da
conf = &config{}
}
if failRotateRaw, ok := data.GetOk("fail_rotate"); ok {
conf.FailRotate = failRotateRaw.(bool)
}
if err := conf.ParseAutomatedRotationFields(data); err != nil {
return logical.ErrorResponse(err.Error()), nil
}
@ -89,6 +97,18 @@ func (b *backend) pathConfigRead(ctx context.Context, req *logical.Request, data
configData := map[string]interface{}{}
conf.PopulateAutomatedRotationData(configData)
if conf.HasNonzeroRotationValues() {
resp, err := b.System().GetRotationInformation(ctx, &rotation.RotationInfoRequest{ReqPath: req.Path})
if err != nil {
return nil, err
}
if resp != nil {
configData["expire_time"] = resp.NextVaultRotation.Unix()
configData["creation_time"] = resp.LastVaultRotation.Unix()
configData["ttl"] = int64(resp.TTL)
}
}
return &logical.Response{
Data: configData,
}, nil
@ -127,5 +147,6 @@ func (b *backend) configEntry(ctx context.Context, s logical.Storage) (*config,
}
type config struct {
FailRotate bool
automatedrotationutil.AutomatedRotationParams
}

File diff suppressed because it is too large Load Diff

View File

@ -617,6 +617,16 @@ message GenerateIdentityTokenResponse {
int64 ttl = 2;
}
message RotationInfoRequest {
string mount_path = 1;
}
message RotationInfoReply {
int64 issue_time = 1;
int64 expire_time = 2;
int64 ttl = 3;
}
message RegisterRotationJobRequest {
RotationJobInput job = 1;
}
@ -703,6 +713,9 @@ service SystemView {
// GenerateIdentityToken returns an identity token for the requesting plugin.
rpc GenerateIdentityToken(GenerateIdentityTokenRequest) returns (GenerateIdentityTokenResponse);
// GetRotationInformation returns the time remaining in a rotation job for the requested credential.
rpc GetRotationInformation(RotationInfoRequest) returns (RotationInfoReply);
// RegisterRotationJob returns a rotation ID for the requested plugin credential.
rpc RegisterRotationJob(RegisterRotationJobRequest) returns (RegisterRotationJobResponse);

View File

@ -688,6 +688,7 @@ const (
SystemView_GeneratePasswordFromPolicy_FullMethodName = "/pb.SystemView/GeneratePasswordFromPolicy"
SystemView_ClusterInfo_FullMethodName = "/pb.SystemView/ClusterInfo"
SystemView_GenerateIdentityToken_FullMethodName = "/pb.SystemView/GenerateIdentityToken"
SystemView_GetRotationInformation_FullMethodName = "/pb.SystemView/GetRotationInformation"
SystemView_RegisterRotationJob_FullMethodName = "/pb.SystemView/RegisterRotationJob"
SystemView_DeregisterRotationJob_FullMethodName = "/pb.SystemView/DeregisterRotationJob"
)
@ -741,6 +742,8 @@ type SystemViewClient interface {
ClusterInfo(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*ClusterInfoReply, error)
// GenerateIdentityToken returns an identity token for the requesting plugin.
GenerateIdentityToken(ctx context.Context, in *GenerateIdentityTokenRequest, opts ...grpc.CallOption) (*GenerateIdentityTokenResponse, error)
// GetRotationInformation returns the time remaining in a rotation job for the requested credential.
GetRotationInformation(ctx context.Context, in *RotationInfoRequest, opts ...grpc.CallOption) (*RotationInfoReply, error)
// RegisterRotationJob returns a rotation ID for the requested plugin credential.
RegisterRotationJob(ctx context.Context, in *RegisterRotationJobRequest, opts ...grpc.CallOption) (*RegisterRotationJobResponse, error)
// DeregisterRotationJob returns any errors in de-registering a credential from the Rotation Manager.
@ -895,6 +898,16 @@ func (c *systemViewClient) GenerateIdentityToken(ctx context.Context, in *Genera
return out, nil
}
func (c *systemViewClient) GetRotationInformation(ctx context.Context, in *RotationInfoRequest, opts ...grpc.CallOption) (*RotationInfoReply, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(RotationInfoReply)
err := c.cc.Invoke(ctx, SystemView_GetRotationInformation_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *systemViewClient) RegisterRotationJob(ctx context.Context, in *RegisterRotationJobRequest, opts ...grpc.CallOption) (*RegisterRotationJobResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(RegisterRotationJobResponse)
@ -964,6 +977,8 @@ type SystemViewServer interface {
ClusterInfo(context.Context, *Empty) (*ClusterInfoReply, error)
// GenerateIdentityToken returns an identity token for the requesting plugin.
GenerateIdentityToken(context.Context, *GenerateIdentityTokenRequest) (*GenerateIdentityTokenResponse, error)
// GetRotationInformation returns the time remaining in a rotation job for the requested credential.
GetRotationInformation(context.Context, *RotationInfoRequest) (*RotationInfoReply, error)
// RegisterRotationJob returns a rotation ID for the requested plugin credential.
RegisterRotationJob(context.Context, *RegisterRotationJobRequest) (*RegisterRotationJobResponse, error)
// DeregisterRotationJob returns any errors in de-registering a credential from the Rotation Manager.
@ -1020,6 +1035,9 @@ func (UnimplementedSystemViewServer) ClusterInfo(context.Context, *Empty) (*Clus
func (UnimplementedSystemViewServer) GenerateIdentityToken(context.Context, *GenerateIdentityTokenRequest) (*GenerateIdentityTokenResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GenerateIdentityToken not implemented")
}
func (UnimplementedSystemViewServer) GetRotationInformation(context.Context, *RotationInfoRequest) (*RotationInfoReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetRotationInformation not implemented")
}
func (UnimplementedSystemViewServer) RegisterRotationJob(context.Context, *RegisterRotationJobRequest) (*RegisterRotationJobResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method RegisterRotationJob not implemented")
}
@ -1299,6 +1317,24 @@ func _SystemView_GenerateIdentityToken_Handler(srv interface{}, ctx context.Cont
return interceptor(ctx, in, info, handler)
}
func _SystemView_GetRotationInformation_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RotationInfoRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(SystemViewServer).GetRotationInformation(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: SystemView_GetRotationInformation_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(SystemViewServer).GetRotationInformation(ctx, req.(*RotationInfoRequest))
}
return interceptor(ctx, in, info, handler)
}
func _SystemView_RegisterRotationJob_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RegisterRotationJobRequest)
if err := dec(in); err != nil {
@ -1398,6 +1434,10 @@ var SystemView_ServiceDesc = grpc.ServiceDesc{
MethodName: "GenerateIdentityToken",
Handler: _SystemView_GenerateIdentityToken_Handler,
},
{
MethodName: "GetRotationInformation",
Handler: _SystemView_GetRotationInformation_Handler,
},
{
MethodName: "RegisterRotationJob",
Handler: _SystemView_RegisterRotationJob_Handler,

View File

@ -49,6 +49,25 @@ type RotationJobDeregisterRequest struct {
ReqPath string
}
// RotationInfoRequest is the request struct used by SystemView.GetRotationInformation.
type RotationInfoRequest struct {
// ReqPath is the plugin-local path to the credential, and needs to match the ReqPath value that
// was supplied in schedule creation with RegisterRotationJob
ReqPath string
}
// RotationInfoResponse is the response struct returned by SystemView.GetRotationInformation.
type RotationInfoResponse struct {
// NextVaultRotation is the scheduled time of the next rotation.
NextVaultRotation time.Time
// LastVaultRotation is the time of the prior rotation.
LastVaultRotation time.Time
// TTL is integer seconds until next rotation, conventionally clamped to 0 (i.e., will not be negative)
TTL int64
}
func (s *RotationJob) Validate() error {
if s.MountPoint == "" {
return fmt.Errorf("MountPoint is required")

View File

@ -355,6 +355,17 @@ func (d dynamicSystemView) GenerateIdentityToken(ctx context.Context, req *plugi
}, nil
}
func (d dynamicSystemView) GetRotationInformation(ctx context.Context, req *rotation.RotationInfoRequest) (*rotation.RotationInfoResponse, error) {
// sanity check
mountEntry := d.mountEntry
if mountEntry == nil {
return nil, fmt.Errorf("no mount entry")
}
nsCtx := namespace.ContextWithNamespace(ctx, mountEntry.Namespace())
return d.core.GetRotationInformation(nsCtx, mountEntry.APIPath(), req)
}
func (d dynamicSystemView) RegisterRotationJob(ctx context.Context, req *rotation.RotationJobConfigureRequest) (string, error) {
mountEntry := d.mountEntry
if mountEntry == nil {

View File

@ -24,10 +24,17 @@ func (c *Core) stopRotation() error {
return nil
}
func (c *Core) GetRotationInformation(_ context.Context, _ string, _ *rotation.RotationInfoRequest) (*rotation.RotationInfoResponse, error) {
return nil, automatedrotationutil.ErrRotationManagerUnsupported
}
func (c *Core) RegisterRotationJob(_ context.Context, _ *rotation.RotationJob) (string, error) {
return "", automatedrotationutil.ErrRotationManagerUnsupported
}
// The DeregisterRotationJob stub returns nil instead of an error because it is intended to be valid to send a deregister
// request for a non-existent job. As a result, the plugin sends a deregister request whenever the relevant rotation
// values are unset. This means that for a plugin running in CE Vault, it will _always_ try to send a deregister request.
func (c *Core) DeregisterRotationJob(_ context.Context, _ *rotation.RotationJobDeregisterRequest) error {
return nil
}