diff --git a/cmd/ateapi/internal/actoridentity/actoridentity.go b/cmd/ateapi/internal/actoridentity/actoridentity.go index 665f351527..7304c43d15 100644 --- a/cmd/ateapi/internal/actoridentity/actoridentity.go +++ b/cmd/ateapi/internal/actoridentity/actoridentity.go @@ -28,6 +28,7 @@ import ( "time" "github.com/agent-substrate/substrate/cmd/ateapi/internal/actoridjwt" + "github.com/agent-substrate/substrate/cmd/ateapi/internal/controlapi" "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" "github.com/agent-substrate/substrate/cmd/ateapi/internal/workercache" "github.com/agent-substrate/substrate/internal/localca" @@ -40,6 +41,7 @@ import ( "google.golang.org/grpc/credentials" "google.golang.org/grpc/peer" "google.golang.org/grpc/status" + "k8s.io/apimachinery/pkg/api/operation" "k8s.io/apimachinery/pkg/util/validation/field" ) @@ -94,6 +96,10 @@ func (s *Server) MintJWT(ctx context.Context, req *ateapipb.MintJWTRequest) (*at return nil, status.Errorf(codes.PermissionDenied, "caller is not permitted to mint actor JWTs") } + if errs := validateMintJWTRequest(ctx, req); len(errs) > 0 { + return nil, status.Error(codes.InvalidArgument, errs.ToAggregate().Error()) + } + // TODO: Cross-check the verified caller and requested actor against the actor database. // TODO: Cache signing keys in memory, so we don't read from disk every time. @@ -106,10 +112,6 @@ func (s *Server) MintJWT(ctx context.Context, req *ateapipb.MintJWTRequest) (*at if err != nil { return nil, fmt.Errorf("while unmarshaling signing pool: %w", err) } - // We only issue tokens with audience bindings. - if len(req.GetAudience()) == 0 { - return nil, fmt.Errorf("at least one audience must be requested") - } actorClaims := &actoridjwt.Claims{ // TODO: This is currently API but it has to be a globally unique, oidc-compliant and accsible DNS name @@ -150,16 +152,14 @@ func (s *Server) MintCert(ctx context.Context, req *ateapipb.MintCertRequest) (* if err != nil { return nil, err } + if errs := validateMintCertRequest(ctx, req); len(errs) > 0 { + return nil, status.Error(codes.InvalidArgument, errs.ToAggregate().Error()) + } + // Validation bounds purpose to the enum's range; which purposes this + // server actually supports is a policy decision that stays here. if req.GetPurpose() != ateapipb.ActorCertificatePurpose_ACTOR_CERTIFICATE_PURPOSE_ATUNNEL { return nil, status.Error(codes.InvalidArgument, "unsupported actor certificate purpose") } - - if err := validateWorkerRef(req.GetWorker()); err != nil { - return nil, status.Errorf(codes.InvalidArgument, "invalid worker: %v", err) - } - if req.GetExpectedActorUid() == "" { - return nil, status.Error(codes.InvalidArgument, "expected_actor_uid is required") - } actor, actorRef, err := s.authorizeActor(ctx, caller, req) if err != nil { return nil, err @@ -286,10 +286,16 @@ func authenticateAtelet(ctx context.Context) (*ateletCaller, error) { return &ateletCaller{podName: identity.PodName, nodeName: identity.NodeName}, nil } -// validateWorkerRef checks the reference to the Worker the certificate is -// minted for. Workers are global-scoped, so the reference carries no atespace. -func validateWorkerRef(worker *ateapipb.ObjectRef) error { - return resources.ValidateGlobalObjectRef(worker, field.NewPath("worker")).ToAggregate() +func validateMintJWTRequest(ctx context.Context, req *ateapipb.MintJWTRequest) field.ErrorList { + // Call the generated validation. + op := operation.Operation{Type: operation.Create} + return controlapi.Validate_MintJWTRequest(ctx, op, nil, req, nil) +} + +func validateMintCertRequest(ctx context.Context, req *ateapipb.MintCertRequest) field.ErrorList { + // Call the generated validation. + op := operation.Operation{Type: operation.Create} + return controlapi.Validate_MintCertRequest(ctx, op, nil, req, nil) } // authorizeActor resolves the actor from the authenticated worker and verifies diff --git a/cmd/ateapi/internal/actoridentity/actoridentity_test.go b/cmd/ateapi/internal/actoridentity/actoridentity_test.go index e236f775b9..52d0964572 100644 --- a/cmd/ateapi/internal/actoridentity/actoridentity_test.go +++ b/cmd/ateapi/internal/actoridentity/actoridentity_test.go @@ -21,9 +21,11 @@ import ( "crypto/tls" "crypto/x509" "crypto/x509/pkix" + "fmt" "math/big" "net/url" "path" + "strings" "testing" "time" @@ -39,8 +41,14 @@ import ( "google.golang.org/grpc/credentials" "google.golang.org/grpc/peer" "google.golang.org/grpc/status" + "k8s.io/apimachinery/pkg/util/validation/field" ) +func assertValidateErr(t *testing.T, got field.ErrorList, want field.ErrorList) { + t.Helper() + field.ErrorMatcher{}.ByType().ByField().ByOrigin().Test(t, want, got) +} + const ( testAtespace = "team-alpha" testActorName = "counter-1" @@ -777,7 +785,7 @@ func TestMintCertActorUID(t *testing.T) { wantCode codes.Code }{ "Matching": {requestUID: func(actorUID string) string { return actorUID }, wantCode: codes.OK}, - "Stale": {requestUID: func(string) string { return "uid-of-a-previous-incarnation" }, wantCode: codes.FailedPrecondition}, + "Stale": {requestUID: func(string) string { return "9d1f7b06-3c58-4a2e-8b40-5f7c1e9a2d63" }, wantCode: codes.FailedPrecondition}, } { t.Run(name, func(t *testing.T) { leaf, actorUID, err := mintCertFor(t, func(actorUID string) *ateapipb.MintCertRequest { @@ -924,3 +932,154 @@ func TestMintCertAuthorizesBeforeSigning(t *testing.T) { t.Errorf("MintCert() code = %v (err = %v), want %v", got, err, codes.PermissionDenied) } } + +func TestValidateMintJWTRequest(t *testing.T) { + // This test verifies validation of user input for minting a JWT. + validReq := func(mods ...func(req *ateapipb.MintJWTRequest)) *ateapipb.MintJWTRequest { + req := &ateapipb.MintJWTRequest{ + Audience: []string{"aud1"}, + Atespace: "as1", + ActorName: "actor1", + ActorUid: "01234567-89ab-cdef-0123-456789abcdef", + } + for _, m := range mods { + m(req) + } + return req + } + + tests := []struct { + name string + req *ateapipb.MintJWTRequest + want field.ErrorList + }{{ + "valid", + validReq(), + nil, + }, { + "missing audience", + validReq(func(r *ateapipb.MintJWTRequest) { r.Audience = nil }), + field.ErrorList{field.Required(field.NewPath("audience"), "")}, + }, { + "too many audiences", + validReq(func(r *ateapipb.MintJWTRequest) { + r.Audience = make([]string, 17) + for i := range r.Audience { + r.Audience[i] = fmt.Sprintf("https://svc-%d.example.com", i) + } + }), + field.ErrorList{field.TooMany(field.NewPath("audience"), 17, 16).WithOrigin("maxItems")}, + }, { + "duplicate audience entry", + validReq(func(r *ateapipb.MintJWTRequest) { + r.Audience = []string{"https://a.example.com", "https://a.example.com"} + }), + field.ErrorList{field.Duplicate(field.NewPath("audience").Index(1), nil)}, + }, { + "audience entry too long", + validReq(func(r *ateapipb.MintJWTRequest) { r.Audience = []string{strings.Repeat("a", 513)} }), + field.ErrorList{field.TooLong(field.NewPath("audience").Index(0), nil, 512).WithOrigin("maxLength")}, + }, { + "missing atespace", + validReq(func(r *ateapipb.MintJWTRequest) { r.Atespace = "" }), + field.ErrorList{field.Required(field.NewPath("atespace"), "")}, + }, { + "invalid atespace", + validReq(func(r *ateapipb.MintJWTRequest) { r.Atespace = "AS1" }), + field.ErrorList{field.Invalid(field.NewPath("atespace"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + "missing actor_name", + validReq(func(r *ateapipb.MintJWTRequest) { r.ActorName = "" }), + field.ErrorList{field.Required(field.NewPath("actor_name"), "")}, + }, { + "invalid actor_name", + validReq(func(r *ateapipb.MintJWTRequest) { r.ActorName = "invalid value" }), + field.ErrorList{field.Invalid(field.NewPath("actor_name"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + "unspecified actor_uid", + validReq(func(r *ateapipb.MintJWTRequest) { r.ActorUid = "" }), + nil, + }, { + "invalid actor_uid", + validReq(func(r *ateapipb.MintJWTRequest) { r.ActorUid = "not a uid" }), + field.ErrorList{field.Invalid(field.NewPath("actor_uid"), nil, "").WithOrigin("format=k8s-uuid")}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertValidateErr(t, validateMintJWTRequest(context.Background(), tt.req), tt.want) + }) + } +} + +func TestValidateMintCertRequest(t *testing.T) { + // This test verifies validation of user input for minting a certificate. + validReq := func(mods ...func(req *ateapipb.MintCertRequest)) *ateapipb.MintCertRequest { + req := &ateapipb.MintCertRequest{ + Worker: &ateapipb.ObjectRef{Name: "worker1"}, + CertificateSigningRequest: []byte{0x01}, + ExpectedActorUid: "01234567-89ab-cdef-0123-456789abcdef", + Purpose: ateapipb.ActorCertificatePurpose_ACTOR_CERTIFICATE_PURPOSE_ATUNNEL, + } + for _, m := range mods { + m(req) + } + return req + } + + tests := []struct { + name string + req *ateapipb.MintCertRequest + want field.ErrorList + }{{ + "valid", + validReq(), + nil, + }, { + "oversized certificate_signing_request", + validReq(func(r *ateapipb.MintCertRequest) { r.CertificateSigningRequest = make([]byte, 16385) }), + field.ErrorList{field.TooLong(field.NewPath("certificate_signing_request"), nil, 16384)}, + }, { + "missing worker", + validReq(func(r *ateapipb.MintCertRequest) { r.Worker = nil }), + field.ErrorList{field.Required(field.NewPath("worker"), "")}, + }, { + "worker.atespace must be empty", + validReq(func(r *ateapipb.MintCertRequest) { r.Worker.Atespace = "as1" }), + field.ErrorList{field.Forbidden(field.NewPath("worker", "atespace"), "")}, + }, { + "missing worker.name", + validReq(func(r *ateapipb.MintCertRequest) { r.Worker.Name = "" }), + field.ErrorList{field.Required(field.NewPath("worker", "name"), "")}, + }, { + "invalid worker.name", + validReq(func(r *ateapipb.MintCertRequest) { r.Worker.Name = "invalid value" }), + field.ErrorList{field.Invalid(field.NewPath("worker", "name"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + "missing certificate_signing_request", + validReq(func(r *ateapipb.MintCertRequest) { r.CertificateSigningRequest = nil }), + field.ErrorList{field.Required(field.NewPath("certificate_signing_request"), "")}, + }, { + "missing expected_actor_uid", + validReq(func(r *ateapipb.MintCertRequest) { r.ExpectedActorUid = "" }), + field.ErrorList{field.Required(field.NewPath("expected_actor_uid"), "")}, + }, { + "invalid expected_actor_uid", + validReq(func(r *ateapipb.MintCertRequest) { r.ExpectedActorUid = "not a uid" }), + field.ErrorList{field.Invalid(field.NewPath("expected_actor_uid"), nil, "").WithOrigin("format=k8s-uuid")}, + }, { + "unspecified purpose", + validReq(func(r *ateapipb.MintCertRequest) { + r.Purpose = ateapipb.ActorCertificatePurpose_ACTOR_CERTIFICATE_PURPOSE_UNSPECIFIED + }), + field.ErrorList{field.Required(field.NewPath("purpose"), "")}, + }, { + "out-of-range purpose", + validReq(func(r *ateapipb.MintCertRequest) { r.Purpose = ateapipb.ActorCertificatePurpose(99) }), + field.ErrorList{field.Invalid(field.NewPath("purpose"), nil, "").WithOrigin("maximum")}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertValidateErr(t, validateMintCertRequest(context.Background(), tt.req), tt.want) + }) + } +} diff --git a/cmd/ateapi/internal/controlapi/validate.go b/cmd/ateapi/internal/controlapi/validate.go index 68358d20ee..630da536f7 100644 --- a/cmd/ateapi/internal/controlapi/validate.go +++ b/cmd/ateapi/internal/controlapi/validate.go @@ -97,3 +97,14 @@ func ValidateCustom_UpdateActorRequest_Actor(ctx context.Context, op operation.O func ValidateCustom_WorkerAssignment_WorkerPodIp(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { return validation.IsValidIP(fldPath, *value) } + +// maxCSRBytes bounds MintCertRequest's CSR. Real CSRs are a few KB; this is +// a guardrail, applied here because maxLength does not support bytes fields. +const maxCSRBytes = 16384 + +func ValidateCustom_MintCertRequest_CertificateSigningRequest(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ []byte) field.ErrorList { + if len(value) > maxCSRBytes { + return field.ErrorList{field.TooLong(fldPath, nil, maxCSRBytes)} + } + return nil +} diff --git a/cmd/ateapi/internal/controlapi/zz_generated.validation.go b/cmd/ateapi/internal/controlapi/zz_generated.validation.go index c15eaf25c3..3e04ef47a4 100644 --- a/cmd/ateapi/internal/controlapi/zz_generated.validation.go +++ b/cmd/ateapi/internal/controlapi/zz_generated.validation.go @@ -1684,6 +1684,312 @@ func Validate_ListAtespacesRequest( return errs } +// Validate_MintCertRequest validates an instance of MintCertRequest according +// to declarative validation rules in the API schema. +func Validate_MintCertRequest( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateapipb.MintCertRequest) (errs field.ErrorList) { + + { // field ateapipb.MintCertRequest.Worker + fn := func( + fldPath *field.Path, + obj, oldObj *ateapipb.ObjectRef, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + func() { // cohort = "atespace" + earlyReturn := false + if e := validate.Subfield(ctx, op, fldPath, obj, oldObj, "atespace", + func(o *ateapipb.ObjectRef) *string { return &o.Atespace }, validate.DirectEqual, validate.ForbiddenValue).MarkBeta().MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.Subfield(ctx, op, fldPath, obj, oldObj, "atespace", + func(o *ateapipb.ObjectRef) *string { return &o.Atespace }, validate.DirectEqual, validate.OptionalValue).MarkBeta().MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if e := validate.Subfield(ctx, op, fldPath, obj, oldObj, "atespace", + func(o *ateapipb.ObjectRef) *string { return &o.Atespace }, validate.DirectEqual, validate.OptionalValue).MarkBeta().MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + }() + // call the type's validation function + errs = append(errs, Validate_ObjectRef(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.MintCertRequest) *ateapipb.ObjectRef { + return oldObj.Worker + }) + errs = append(errs, fn(fldPath.Child("worker"), obj.Worker, oldVal, oldObj != nil)...) + } + + { // field ateapipb.MintCertRequest.CertificateSigningRequest + fn := func( + fldPath *field.Path, + obj, oldObj []byte, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_MintCertRequest_CertificateSigningRequest(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.MintCertRequest) []byte { + return oldObj.CertificateSigningRequest + }) + errs = append(errs, fn(fldPath.Child("certificate_signing_request"), obj.CertificateSigningRequest, oldVal, oldObj != nil)...) + } + + { // field ateapipb.MintCertRequest.ExpectedActorUid + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.UUID(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.MintCertRequest) *string { + return &oldObj.ExpectedActorUid + }) + errs = append(errs, fn(fldPath.Child("expected_actor_uid"), &obj.ExpectedActorUid, oldVal, oldObj != nil)...) + } + + { // field ateapipb.MintCertRequest.Purpose + fn := func( + fldPath *field.Path, + obj, oldObj *ateapipb.ActorCertificatePurpose, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.Maximum(ctx, op, fldPath, obj, oldObj, 1); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.Minimum(ctx, op, fldPath, obj, oldObj, 1); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.MintCertRequest) *ateapipb.ActorCertificatePurpose { + return &oldObj.Purpose + }) + errs = append(errs, fn(fldPath.Child("purpose"), &obj.Purpose, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_MintJWTRequest validates an instance of MintJWTRequest according +// to declarative validation rules in the API schema. +func Validate_MintJWTRequest( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateapipb.MintJWTRequest) (errs field.ErrorList) { + + { // field ateapipb.MintJWTRequest.Audience + fn := func( + fldPath *field.Path, + obj, oldObj []string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 16).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.RequiredSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.EachValSliceVal(ctx, op, fldPath, obj, oldObj, validate.DirectEqual, nil, + func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + return validate.MaxLength(ctx, op, fldPath, obj, oldObj, 512) + }); len(e) != 0 { + errs = append(errs, e...) + } + // lists with set semantics require unique values + if e := validate.ValSliceUnique(ctx, op, fldPath, obj, oldObj, validate.DirectEqual); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.MintJWTRequest) []string { + return oldObj.Audience + }) + errs = append(errs, fn(fldPath.Child("audience"), obj.Audience, oldVal, oldObj != nil)...) + } + + { // field ateapipb.MintJWTRequest.Atespace + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.MintJWTRequest) *string { + return &oldObj.Atespace + }) + errs = append(errs, fn(fldPath.Child("atespace"), &obj.Atespace, oldVal, oldObj != nil)...) + } + + { // field ateapipb.MintJWTRequest.ActorName + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.MintJWTRequest) *string { + return &oldObj.ActorName + }) + errs = append(errs, fn(fldPath.Child("actor_name"), &obj.ActorName, oldVal, oldObj != nil)...) + } + + { // field ateapipb.MintJWTRequest.ActorUid + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.UUID(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.MintJWTRequest) *string { + return &oldObj.ActorUid + }) + errs = append(errs, fn(fldPath.Child("actor_uid"), &obj.ActorUid, oldVal, oldObj != nil)...) + } + + return errs +} + // Validate_ObjectRef validates an instance of ObjectRef according // to declarative validation rules in the API schema. func Validate_ObjectRef( diff --git a/pkg/proto/ateapipb/ateapi.pb.go b/pkg/proto/ateapipb/ateapi.pb.go index c9adccd53f..f84c62275e 100644 --- a/pkg/proto/ateapipb/ateapi.pb.go +++ b/pkg/proto/ateapipb/ateapi.pb.go @@ -423,7 +423,7 @@ type ActorCertificatePurpose int32 const ( ActorCertificatePurpose_ACTOR_CERTIFICATE_PURPOSE_UNSPECIFIED ActorCertificatePurpose = 0 - ActorCertificatePurpose_ACTOR_CERTIFICATE_PURPOSE_ATUNNEL ActorCertificatePurpose = 1 + ActorCertificatePurpose_ACTOR_CERTIFICATE_PURPOSE_ATUNNEL ActorCertificatePurpose = 1 // Keep this in sync with MintCertRequest.purpose's maximum. ) // Enum value maps for ActorCertificatePurpose. @@ -6075,11 +6075,24 @@ func (*DebugClearResponse) Descriptor() ([]byte, []int) { } type MintJWTRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Audience []string `protobuf:"bytes,1,rep,name=audience,proto3" json:"audience,omitempty"` - Atespace string `protobuf:"bytes,2,opt,name=atespace,proto3" json:"atespace,omitempty"` - ActorName string `protobuf:"bytes,3,opt,name=actor_name,json=actorName,proto3" json:"actor_name,omitempty"` - ActorUid string `protobuf:"bytes,4,opt,name=actor_uid,json=actorUid,proto3" json:"actor_uid,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // The audiences the minted JWT is bound to. Tokens are only issued with + // audience bindings, so at least one is required. + // + // +k8s:required + // +k8s:maxItems=16 # guardrail; tokens realistically bind a handful of audiences + // +k8s:listType=set + // +k8s:eachVal=+k8s:maxLength=512 # audiences are caller-defined URIs; bound only + Audience []string `protobuf:"bytes,1,rep,name=audience,proto3" json:"audience,omitempty"` + // +k8s:required + // +k8s:format=k8s-short-name + Atespace string `protobuf:"bytes,2,opt,name=atespace,proto3" json:"atespace,omitempty"` + // +k8s:required + // +k8s:format=k8s-short-name + ActorName string `protobuf:"bytes,3,opt,name=actor_name,json=actorName,proto3" json:"actor_name,omitempty"` + // +k8s:optional + // +k8s:format=k8s-uuid + ActorUid string `protobuf:"bytes,4,opt,name=actor_uid,json=actorUid,proto3" json:"actor_uid,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -6215,18 +6228,29 @@ type MintCertRequest struct { // This is the one caller that recovers a Worker name from a pod certificate: // the atelet has only the worker Pod's identity to go on. Everywhere else the // name is opaque and must be carried, not reconstructed. - // +k8s:opaqueType + // + // +k8s:beta(since: "0.0")=+k8s:subfield(atespace)=+k8s:forbidden # TODO: get rid of beta prefix + // +k8s:required Worker *ObjectRef `protobuf:"bytes,1,opt,name=worker,proto3" json:"worker,omitempty"` // Request contains DER encoded bytes of a x509 certificate signing request. // The signer will ignore the contents of the CSR except to extract the // subject public key. + // + // +k8s:required + // +k8s:customValidation # size bound; maxLength is string-only CertificateSigningRequest []byte `protobuf:"bytes,2,opt,name=certificate_signing_request,json=certificateSigningRequest,proto3" json:"certificate_signing_request,omitempty"` // Actor incarnation expected by the activation. This is only a stale-request // guard: ateapi derives the actor and its identity from the worker assignment. - ExpectedActorUid string `protobuf:"bytes,3,opt,name=expected_actor_uid,json=expectedActorUid,proto3" json:"expected_actor_uid,omitempty"` - Purpose ActorCertificatePurpose `protobuf:"varint,4,opt,name=purpose,proto3,enum=ateapi.ActorCertificatePurpose" json:"purpose,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // + // +k8s:required + // +k8s:format=k8s-uuid + ExpectedActorUid string `protobuf:"bytes,3,opt,name=expected_actor_uid,json=expectedActorUid,proto3" json:"expected_actor_uid,omitempty"` + // +k8s:required + // +k8s:minimum=1 + // +k8s:maximum=1 # keep this in sync with the ActorCertificatePurpose enum + Purpose ActorCertificatePurpose `protobuf:"varint,4,opt,name=purpose,proto3,enum=ateapi.ActorCertificatePurpose" json:"purpose,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *MintCertRequest) Reset() { diff --git a/pkg/proto/ateapipb/ateapi.proto b/pkg/proto/ateapipb/ateapi.proto index 9bf5a80064..6ca24e44fc 100644 --- a/pkg/proto/ateapipb/ateapi.proto +++ b/pkg/proto/ateapipb/ateapi.proto @@ -1416,10 +1416,25 @@ service ActorIdentity { } message MintJWTRequest { + // The audiences the minted JWT is bound to. Tokens are only issued with + // audience bindings, so at least one is required. + // + // +k8s:required + // +k8s:maxItems=16 # guardrail; tokens realistically bind a handful of audiences + // +k8s:listType=set + // +k8s:eachVal=+k8s:maxLength=512 # audiences are caller-defined URIs; bound only repeated string audience = 1; + // +k8s:required + // +k8s:format=k8s-short-name string atespace = 2; + + // +k8s:required + // +k8s:format=k8s-short-name string actor_name = 3; + + // +k8s:optional + // +k8s:format=k8s-uuid string actor_uid = 4; } @@ -1455,24 +1470,36 @@ message MintCertRequest { // This is the one caller that recovers a Worker name from a pod certificate: // the atelet has only the worker Pod's identity to go on. Everywhere else the // name is opaque and must be carried, not reconstructed. - // +k8s:opaqueType + // + // +k8s:beta(since: "0.0")=+k8s:subfield(atespace)=+k8s:forbidden # TODO: get rid of beta prefix + // +k8s:required ObjectRef worker = 1; // Request contains DER encoded bytes of a x509 certificate signing request. // The signer will ignore the contents of the CSR except to extract the // subject public key. + // + // +k8s:required + // +k8s:customValidation # size bound; maxLength is string-only bytes certificate_signing_request = 2; // Actor incarnation expected by the activation. This is only a stale-request // guard: ateapi derives the actor and its identity from the worker assignment. + // + // +k8s:required + // +k8s:format=k8s-uuid string expected_actor_uid = 3; + // +k8s:required + // +k8s:minimum=1 + // +k8s:maximum=1 # keep this in sync with the ActorCertificatePurpose enum ActorCertificatePurpose purpose = 4; } enum ActorCertificatePurpose { ACTOR_CERTIFICATE_PURPOSE_UNSPECIFIED = 0; ACTOR_CERTIFICATE_PURPOSE_ATUNNEL = 1; + // Keep this in sync with MintCertRequest.purpose's maximum. } message MintCertResponse {