tokencredentialrequest audit logs successful responses

Co-authored-by: Joshua Casey <joshuatcasey@gmail.com>
This commit is contained in:
Ryan Richard
2024-11-27 13:53:01 -06:00
committed by Joshua Casey
co-authored by Joshua Casey
parent 37e12b4024
commit e21e1326b7
3 changed files with 101 additions and 36 deletions
+8 -1
View File
@@ -15,6 +15,7 @@ import (
"k8s.io/apiserver/pkg/registry/rest"
genericapiserver "k8s.io/apiserver/pkg/server"
utilversion "k8s.io/apiserver/pkg/util/version"
"k8s.io/utils/clock"
"go.pinniped.dev/internal/clientcertissuer"
"go.pinniped.dev/internal/controllerinit"
@@ -83,7 +84,13 @@ func (c completedConfig) New() (*PinnipedServer, error) {
for _, f := range []func() (schema.GroupVersionResource, rest.Storage){
func() (schema.GroupVersionResource, rest.Storage) {
tokenCredReqGVR := c.ExtraConfig.LoginConciergeGroupVersion.WithResource("tokencredentialrequests")
tokenCredStorage := credentialrequest.NewREST(c.ExtraConfig.Authenticator, c.ExtraConfig.Issuer, tokenCredReqGVR.GroupResource(), c.ExtraConfig.AuditLogger)
tokenCredStorage := credentialrequest.NewREST(
c.ExtraConfig.Authenticator,
c.ExtraConfig.Issuer,
tokenCredReqGVR.GroupResource(),
c.ExtraConfig.AuditLogger,
clock.RealClock{},
)
return tokenCredReqGVR, tokenCredStorage
},
func() (schema.GroupVersionResource, rest.Storage) {
+5 -1
View File
@@ -18,6 +18,7 @@ import (
"k8s.io/apiserver/pkg/authentication/user"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest"
"k8s.io/utils/clock"
"k8s.io/utils/trace"
loginapi "go.pinniped.dev/generated/latest/apis/concierge/login"
@@ -38,12 +39,14 @@ func NewREST(
issuer clientcertissuer.ClientCertIssuer,
resource schema.GroupResource,
auditLogger plog.AuditLogger,
clock clock.Clock,
) *REST {
return &REST{
authenticator: authenticator,
issuer: issuer,
tableConvertor: rest.NewDefaultTableConvertor(resource),
auditLogger: auditLogger,
clock: clock,
}
}
@@ -52,6 +55,7 @@ type REST struct {
issuer clientcertissuer.ClientCertIssuer
tableConvertor rest.TableConvertor
auditLogger plog.AuditLogger
clock clock.Clock
}
// Assert that our *REST implements all the optional interfaces that we expect it to implement.
@@ -123,7 +127,7 @@ func (r *REST) Create(ctx context.Context, obj runtime.Object, createValidation
}
// this timestamp should be returned from IssueClientCertPEM but this is a safe approximation
expires := metav1.NewTime(time.Now().UTC().Add(clientCertificateTTL))
expires := metav1.NewTime(r.clock.Now().UTC().Add(clientCertificateTTL))
certPEM, keyPEM, err := r.issuer.IssueClientCertPEM(userInfo.GetName(), userInfo.GetGroups(), clientCertificateTTL)
if err != nil {
traceFailureWithError(t, "cert issuer", err)
@@ -4,6 +4,7 @@
package credentialrequest
import (
"bytes"
"context"
"errors"
"fmt"
@@ -18,10 +19,13 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/audit"
"k8s.io/apiserver/pkg/authentication/user"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest"
"k8s.io/klog/v2"
"k8s.io/utils/clock"
clocktesting "k8s.io/utils/clock/testing"
"k8s.io/utils/ptr"
loginapi "go.pinniped.dev/generated/latest/apis/concierge/login"
@@ -33,7 +37,7 @@ import (
)
func TestNew(t *testing.T) {
r := NewREST(nil, nil, schema.GroupResource{Group: "bears", Resource: "panda"}, nil)
r := NewREST(nil, nil, schema.GroupResource{Group: "bears", Resource: "panda"}, nil, clock.RealClock{})
require.NotNil(t, r)
require.False(t, r.NamespaceScoped())
require.Equal(t, []string{"pinniped"}, r.Categories())
@@ -71,6 +75,10 @@ func TestCreate(t *testing.T) {
var logger *testutil.TranscriptLogger
var originalKLogLevel klog.Level
var auditLogger plog.AuditLogger
var actualAuditLog *bytes.Buffer
var frozenNow time.Time
var frozenClock *clocktesting.FakeClock
var wantAuditLog []testutil.WantedAuditLog
it.Before(func() {
r = require.New(t)
@@ -80,10 +88,13 @@ func TestCreate(t *testing.T) {
originalKLogLevel = testutil.GetGlobalKlogLevel()
// trace.Log() utility will only log at level 2 or above, so set that for this test.
testutil.SetGlobalKlogLevel(t, 2) //nolint:staticcheck // old test of code using trace.Log()
auditLogger, _ = plog.TestAuditLogger(t)
auditLogger, actualAuditLog = plog.TestAuditLogger(t)
frozenNow = time.Date(2024, time.September, 12, 4, 25, 56, 778899, time.UTC)
frozenClock = clocktesting.NewFakeClock(frozenNow)
})
it.After(func() {
testutil.CompareAuditLogs(t, wantAuditLog, actualAuditLog.String())
klog.ClearLogger()
testutil.SetGlobalKlogLevel(t, originalKLogLevel) //nolint:staticcheck // old test of code using trace.Log()
ctrl.Finish()
@@ -106,28 +117,36 @@ func TestCreate(t *testing.T) {
5*time.Minute,
).Return([]byte("test-cert"), []byte("test-key"), nil)
storage := NewREST(requestAuthenticator, clientCertIssuer, schema.GroupResource{}, auditLogger)
storage := NewREST(requestAuthenticator, clientCertIssuer, schema.GroupResource{}, auditLogger, frozenClock)
response, err := callCreate(context.Background(), storage, req)
response, err := callCreate(storage, req)
r.NoError(err)
r.IsType(&loginapi.TokenCredentialRequest{}, response)
expires := response.(*loginapi.TokenCredentialRequest).Status.Credential.ExpirationTimestamp
r.NotNil(expires)
r.InDelta(time.Now().Add(5*time.Minute).Unix(), expires.Unix(), 5)
response.(*loginapi.TokenCredentialRequest).Status.Credential.ExpirationTimestamp = metav1.Time{}
r.Equal(response, &loginapi.TokenCredentialRequest{
Status: loginapi.TokenCredentialRequestStatus{
Credential: &loginapi.ClusterCredential{
ExpirationTimestamp: metav1.Time{},
ExpirationTimestamp: metav1.NewTime(frozenNow.Add(5 * time.Minute).UTC()),
ClientCertificateData: "test-cert",
ClientKeyData: "test-key",
},
},
})
requireOneLogStatement(r, logger, `"success" userID:,hasExtra:false,authenticated:true`)
wantAuditLog = []testutil.WantedAuditLog{
testutil.WantAuditLog("TokenCredentialRequest", map[string]any{
"auditID": "fake-audit-id",
"authenticated": true,
"expires": "2024-09-12T04:30:56Z", // this is frozenNow + 5 minutes in UTC
"personalInfo": map[string]any{
"username": "test-user",
"groups": []any{"test-group-1", "test-group-2"},
},
}),
}
})
it("CreateFailsWithValidTokenWhenCertIssuerFails", func() {
@@ -145,9 +164,9 @@ func TestCreate(t *testing.T) {
IssueClientCertPEM(gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil, nil, fmt.Errorf("some certificate authority error"))
storage := NewREST(requestAuthenticator, clientCertIssuer, schema.GroupResource{}, auditLogger)
storage := NewREST(requestAuthenticator, clientCertIssuer, schema.GroupResource{}, auditLogger, frozenClock)
response, err := callCreate(context.Background(), storage, req)
response, err := callCreate(storage, req)
requireSuccessfulResponseWithAuthenticationFailureMessage(t, err, response)
requireOneLogStatement(r, logger, `"failure" failureType:cert issuer,msg:some certificate authority error`)
})
@@ -158,9 +177,9 @@ func TestCreate(t *testing.T) {
requestAuthenticator := mockcredentialrequest.NewMockTokenCredentialRequestAuthenticator(ctrl)
requestAuthenticator.EXPECT().AuthenticateTokenCredentialRequest(gomock.Any(), req).Return(nil, nil)
storage := NewREST(requestAuthenticator, nil, schema.GroupResource{}, auditLogger)
storage := NewREST(requestAuthenticator, nil, schema.GroupResource{}, auditLogger, frozenClock)
response, err := callCreate(context.Background(), storage, req)
response, err := callCreate(storage, req)
requireSuccessfulResponseWithAuthenticationFailureMessage(t, err, response)
requireOneLogStatement(r, logger, `"success" userID:<none>,hasExtra:false,authenticated:false`)
@@ -173,9 +192,9 @@ func TestCreate(t *testing.T) {
requestAuthenticator.EXPECT().AuthenticateTokenCredentialRequest(gomock.Any(), req).
Return(nil, errors.New("some webhook error"))
storage := NewREST(requestAuthenticator, nil, schema.GroupResource{}, auditLogger)
storage := NewREST(requestAuthenticator, nil, schema.GroupResource{}, auditLogger, frozenClock)
response, err := callCreate(context.Background(), storage, req)
response, err := callCreate(storage, req)
requireSuccessfulResponseWithAuthenticationFailureMessage(t, err, response)
requireOneLogStatement(r, logger, `"failure" failureType:token authentication,msg:some webhook error`)
@@ -188,9 +207,9 @@ func TestCreate(t *testing.T) {
requestAuthenticator.EXPECT().AuthenticateTokenCredentialRequest(gomock.Any(), req).
Return(&user.DefaultInfo{Name: ""}, nil)
storage := NewREST(requestAuthenticator, nil, schema.GroupResource{}, auditLogger)
storage := NewREST(requestAuthenticator, nil, schema.GroupResource{}, auditLogger, frozenClock)
response, err := callCreate(context.Background(), storage, req)
response, err := callCreate(storage, req)
requireSuccessfulResponseWithAuthenticationFailureMessage(t, err, response)
requireOneLogStatement(r, logger, `"success" userID:,hasExtra:false,authenticated:false`)
@@ -207,9 +226,9 @@ func TestCreate(t *testing.T) {
Groups: []string{"test-group-1", "test-group-2"},
}, nil)
storage := NewREST(requestAuthenticator, nil, schema.GroupResource{}, auditLogger)
storage := NewREST(requestAuthenticator, nil, schema.GroupResource{}, auditLogger, frozenClock)
response, err := callCreate(context.Background(), storage, req)
response, err := callCreate(storage, req)
requireSuccessfulResponseWithAuthenticationFailureMessage(t, err, response)
requireOneLogStatement(r, logger, `"success" userID:test-uid,hasExtra:false,authenticated:false`)
@@ -226,9 +245,9 @@ func TestCreate(t *testing.T) {
Extra: map[string][]string{"test-key": {"test-val-1", "test-val-2"}},
}, nil)
storage := NewREST(requestAuthenticator, nil, schema.GroupResource{}, auditLogger)
storage := NewREST(requestAuthenticator, nil, schema.GroupResource{}, auditLogger, frozenClock)
response, err := callCreate(context.Background(), storage, req)
response, err := callCreate(storage, req)
requireSuccessfulResponseWithAuthenticationFailureMessage(t, err, response)
requireOneLogStatement(r, logger, `"success" userID:,hasExtra:true,authenticated:false`)
@@ -236,7 +255,7 @@ func TestCreate(t *testing.T) {
it("CreateFailsWhenGivenTheWrongInputType", func() {
notACredentialRequest := runtime.Unknown{}
response, err := NewREST(nil, nil, schema.GroupResource{}, auditLogger).Create(
response, err := NewREST(nil, nil, schema.GroupResource{}, auditLogger, frozenClock).Create(
genericapirequest.NewContext(),
&notACredentialRequest,
rest.ValidateAllObjectFunc,
@@ -247,8 +266,8 @@ func TestCreate(t *testing.T) {
})
it("CreateFailsWhenTokenValueIsEmptyInRequest", func() {
storage := NewREST(nil, nil, schema.GroupResource{}, auditLogger)
response, err := callCreate(context.Background(), storage, credentialRequest(loginapi.TokenCredentialRequestSpec{
storage := NewREST(nil, nil, schema.GroupResource{}, auditLogger, frozenClock)
response, err := callCreate(storage, credentialRequest(loginapi.TokenCredentialRequestSpec{
Token: "",
}))
@@ -258,7 +277,7 @@ func TestCreate(t *testing.T) {
})
it("CreateFailsWhenValidationFails", func() {
storage := NewREST(nil, nil, schema.GroupResource{}, auditLogger)
storage := NewREST(nil, nil, schema.GroupResource{}, auditLogger, frozenClock)
response, err := storage.Create(
context.Background(),
validCredentialRequest(),
@@ -278,9 +297,12 @@ func TestCreate(t *testing.T) {
requestAuthenticator.EXPECT().AuthenticateTokenCredentialRequest(gomock.Any(), req.DeepCopy()).
Return(&user.DefaultInfo{Name: "test-user"}, nil)
storage := NewREST(requestAuthenticator, successfulIssuer(ctrl), schema.GroupResource{}, auditLogger)
fakeReqContext := audit.WithAuditContext(context.Background())
audit.WithAuditID(fakeReqContext, "fake-audit-id")
storage := NewREST(requestAuthenticator, successfulIssuer(ctrl), schema.GroupResource{}, auditLogger, frozenClock)
response, err := storage.Create(
context.Background(),
fakeReqContext,
req,
func(ctx context.Context, obj runtime.Object) error {
credentialRequest, _ := obj.(*loginapi.TokenCredentialRequest)
@@ -290,6 +312,18 @@ func TestCreate(t *testing.T) {
&metav1.CreateOptions{})
r.NoError(err)
r.NotEmpty(response)
wantAuditLog = []testutil.WantedAuditLog{
testutil.WantAuditLog("TokenCredentialRequest", map[string]any{
"auditID": "fake-audit-id",
"authenticated": true,
"expires": "2024-09-12T04:30:56Z", // this is frozenNow + 5 minutes in UTC
"personalInfo": map[string]any{
"username": "test-user",
"groups": []any{},
},
}),
}
})
it("CreateDoesNotAllowValidationFunctionToSeeTheActualRequestToken", func() {
@@ -299,11 +333,16 @@ func TestCreate(t *testing.T) {
requestAuthenticator.EXPECT().AuthenticateTokenCredentialRequest(gomock.Any(), req.DeepCopy()).
Return(&user.DefaultInfo{Name: "test-user"}, nil)
storage := NewREST(requestAuthenticator, successfulIssuer(ctrl), schema.GroupResource{}, auditLogger)
storage := NewREST(requestAuthenticator, successfulIssuer(ctrl), schema.GroupResource{}, auditLogger, frozenClock)
fakeReqContext := audit.WithAuditContext(context.Background())
audit.WithAuditID(fakeReqContext, "fake-audit-id")
validationFunctionWasCalled := false
var validationFunctionSawTokenValue string
response, err := storage.Create(
context.Background(),
fakeReqContext,
req,
func(ctx context.Context, obj runtime.Object) error {
credentialRequest, _ := obj.(*loginapi.TokenCredentialRequest)
@@ -316,10 +355,22 @@ func TestCreate(t *testing.T) {
r.NotEmpty(response)
r.True(validationFunctionWasCalled)
r.Empty(validationFunctionSawTokenValue)
wantAuditLog = []testutil.WantedAuditLog{
testutil.WantAuditLog("TokenCredentialRequest", map[string]any{
"auditID": "fake-audit-id",
"authenticated": true,
"expires": "2024-09-12T04:30:56Z", // this is frozenNow + 5 minutes in UTC
"personalInfo": map[string]any{
"username": "test-user",
"groups": []any{},
},
}),
}
})
it("CreateFailsWhenRequestOptionsDryRunIsNotEmpty", func() {
response, err := NewREST(nil, nil, schema.GroupResource{}, auditLogger).Create(
response, err := NewREST(nil, nil, schema.GroupResource{}, auditLogger, frozenClock).Create(
genericapirequest.NewContext(),
validCredentialRequest(),
rest.ValidateAllObjectFunc,
@@ -333,7 +384,7 @@ func TestCreate(t *testing.T) {
})
it("CreateFailsWhenNamespaceIsNotEmpty", func() {
response, err := NewREST(nil, nil, schema.GroupResource{}, auditLogger).Create(
response, err := NewREST(nil, nil, schema.GroupResource{}, auditLogger, frozenClock).Create(
genericapirequest.WithNamespace(genericapirequest.NewContext(), "some-ns"),
validCredentialRequest(),
rest.ValidateAllObjectFunc,
@@ -352,9 +403,12 @@ func requireOneLogStatement(r *require.Assertions, logger *testutil.TranscriptLo
r.Contains(transcript[0].Message, messageContains)
}
func callCreate(ctx context.Context, storage *REST, obj runtime.Object) (runtime.Object, error) {
func callCreate(storage *REST, obj runtime.Object) (runtime.Object, error) {
fakeReqContext := audit.WithAuditContext(context.Background())
audit.WithAuditID(fakeReqContext, "fake-audit-id")
return storage.Create(
ctx,
fakeReqContext,
obj,
rest.ValidateAllObjectFunc,
&metav1.CreateOptions{