Skip to content

Commit

Permalink
[scd] Enable USSs to request an OVN for operational intents
Browse files Browse the repository at this point in the history
  • Loading branch information
mickmis committed Sep 20, 2024
1 parent c382e59 commit 1606c4c
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 10 deletions.
29 changes: 29 additions & 0 deletions pkg/scd/models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ package models
import (
"crypto/sha256"
"encoding/base64"
"fmt"
"net/url"
"strings"
"time"

"github.com/google/uuid"
restapi "github.com/interuss/dss/pkg/api/scdv1"
dssmodels "github.com/interuss/dss/pkg/models"
"github.com/interuss/stacktrace"
)

Expand All @@ -20,6 +23,10 @@ const (
// Note that this UUID is not meant to be persisted to the database: it should only be used
// to populate required API fields for which a proper value does not exist.
NullV4UUID = restapi.SubscriptionID("00000000-0000-4000-8000-000000000000")

// maxClockSkew is the largest allowed interval between a client-provided
// time and the server's idea of the current time.
maxClockSkew = time.Minute * 5
)

type (
Expand All @@ -43,6 +50,28 @@ func NewOVNFromTime(t time.Time, salt string) OVN {
return OVN(ovn)
}

// NewOVNFromUUIDv7Suffix returns an OVN based on an UUIDv7 suffix: `{op_intent_id}_{uuidv7_suffix}`.
// It validates that the suffix is indeed a UUIDv7 and that its timestamp is not too far from now.
func NewOVNFromUUIDv7Suffix(now time.Time, oiID dssmodels.ID, suffix string) (OVN, error) {
uuidV7, err := uuid.Parse(suffix)
if err != nil {
return "", stacktrace.Propagate(err, "Suffix `%s` is not a valid UUID", suffix)
}
if uuidV7.Version() != 7 {
return "", stacktrace.NewError("Suffix `%s` is not version 7 but version %d", suffix, uuidV7.Version())
}

var (
ovnTime = time.Unix(uuidV7.Time().UnixTime())
skew = now.Sub(ovnTime).Abs()
)
if skew > maxClockSkew {
return "", stacktrace.NewError("Suffix `%s` is too far away from now (got %s, max is %s)", suffix, skew.String(), maxClockSkew.String())
}

return OVN(fmt.Sprintf("%s_%s", oiID.String(), suffix)), nil
}

// Empty returns true if ovn indicates an empty opaque version number.
func (ovn OVN) Empty() bool {
return len(ovn) == 0
Expand Down
96 changes: 96 additions & 0 deletions pkg/scd/models/models_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,105 @@ import (
"time"

"github.com/google/uuid"
dssmodels "github.com/interuss/dss/pkg/models"
"github.com/stretchr/testify/require"
)

func TestOVNFromTimeIsValid(t *testing.T) {
require.True(t, NewOVNFromTime(time.Now(), uuid.New().String()).Valid())
}

func TestNewOVNFromUUIDv7Suffix(t *testing.T) {
type cases []struct {
name string

now time.Time
oiID dssmodels.ID
suffix string

ovn string
}

t.Run("valid", func(t *testing.T) {
testCases := cases{{
name: "exact",
now: time.Date(2024, time.September, 10, 13, 02, 42, int(408*time.Millisecond), time.UTC),
oiID: "bd65d3de-f52e-419d-acfb-ad85d557de99",
suffix: "0191dc07-76e8-7546-84f2-e739e9f44d77", // 2024-09-10T13:02:42.408Z
ovn: "bd65d3de-f52e-419d-acfb-ad85d557de99_0191dc07-76e8-7546-84f2-e739e9f44d77",
}, {
name: "before",
now: time.Date(2024, time.September, 10, 13, 02, 24, 0, time.UTC),
oiID: "e72589d4-8c14-4d6f-bd9c-1bfb8704e332",
suffix: "0191dc07-2f57-79fd-b021-80456ceb627f", // 2024-09-10T13:02:24.087Z
ovn: "e72589d4-8c14-4d6f-bd9c-1bfb8704e332_0191dc07-2f57-79fd-b021-80456ceb627f",
}, {
name: "after",
now: time.Date(2024, time.September, 10, 13, 02, 48, 0, time.UTC),
oiID: "f577437f-bc6b-4826-9c6b-7831b78eabcc",
suffix: "0191dc07-8a71-7a12-87ed-9baa6e889874", // 2024-09-10T13:02:47.409Z
ovn: "f577437f-bc6b-4826-9c6b-7831b78eabcc_0191dc07-8a71-7a12-87ed-9baa6e889874",
}, {
name: "before - max skew",
now: time.Date(2024, time.September, 10, 12, 57, 25, 0, time.UTC),
oiID: "e72589d4-8c14-4d6f-bd9c-1bfb8704e332",
suffix: "0191dc07-2f57-79fd-b021-80456ceb627f", // 2024-09-10T13:02:24.087Z
ovn: "e72589d4-8c14-4d6f-bd9c-1bfb8704e332_0191dc07-2f57-79fd-b021-80456ceb627f",
}, {
name: "after - max skew",
now: time.Date(2024, time.September, 10, 13, 07, 47, 0, time.UTC),
oiID: "f577437f-bc6b-4826-9c6b-7831b78eabcc",
suffix: "0191dc07-8a71-7a12-87ed-9baa6e889874", // 2024-09-10T13:02:47.409Z
ovn: "f577437f-bc6b-4826-9c6b-7831b78eabcc_0191dc07-8a71-7a12-87ed-9baa6e889874",
}}

for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
ovn, err := NewOVNFromUUIDv7Suffix(testCase.now, testCase.oiID, testCase.suffix)
require.NoError(t, err)
require.EqualValues(t, testCase.ovn, ovn)
})
}
})

t.Run("invalid", func(t *testing.T) {
testCases := cases{{
name: "before - past skew",
now: time.Date(2024, time.September, 10, 12, 57, 24, 0, time.UTC),
oiID: "e72589d4-8c14-4d6f-bd9c-1bfb8704e332",
suffix: "0191dc07-2f57-79fd-b021-80456ceb627f", // 2024-09-10T13:02:24.087Z
}, {
name: "after - past skew",
now: time.Date(2024, time.September, 10, 13, 07, 48, 0, time.UTC),
oiID: "f577437f-bc6b-4826-9c6b-7831b78eabcc",
suffix: "0191dc07-8a71-7a12-87ed-9baa6e889874", // 2024-09-10T13:02:47.409Z
}, {
name: "before - long past skew",
now: time.Date(2024, time.September, 10, 11, 57, 24, 0, time.UTC),
oiID: "e72589d4-8c14-4d6f-bd9c-1bfb8704e332",
suffix: "0191dc07-2f57-79fd-b021-80456ceb627f", // 2024-09-10T13:02:24.087Z
}, {
name: "after - long past skew",
now: time.Date(2024, time.September, 10, 14, 07, 48, 0, time.UTC),
oiID: "f577437f-bc6b-4826-9c6b-7831b78eabcc",
suffix: "0191dc07-8a71-7a12-87ed-9baa6e889874", // 2024-09-10T13:02:47.409Z
}, {
name: "uuidv4",
now: time.Date(2024, time.September, 10, 13, 02, 24, 0, time.UTC),
oiID: "e72589d4-8c14-4d6f-bd9c-1bfb8704e332",
suffix: "44299cb9-a722-4d9c-87bc-537a5aeb2b73",
}, {
name: "not uuid",
now: time.Date(2024, time.September, 10, 13, 02, 24, 0, time.UTC),
oiID: "not_a_uuid",
suffix: "44299cb9-a722-4d9c-87bc-537a5aeb2b73",
}}

for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
_, err := NewOVNFromUUIDv7Suffix(testCase.now, testCase.oiID, testCase.suffix)
require.Error(t, err)
})
}
})
}
4 changes: 0 additions & 4 deletions pkg/scd/models/subscriptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,6 @@ const (
// maxSubscriptionDuration is the largest allowed interval between StartTime
// and EndTime.
maxSubscriptionDuration = time.Hour * 24

// maxClockSkew is the largest allowed interval between the StartTime of a new
// subscription and the server's idea of the current time.
maxClockSkew = time.Minute * 5
)

// Subscription represents an SCD subscription
Expand Down
21 changes: 15 additions & 6 deletions pkg/scd/operational_intents_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ func (a *Server) CreateOperationalIntentReference(ctx context.Context, req *rest
Message: dsserr.Handle(ctx, stacktrace.PropagateWithCode(req.BodyParseError, dsserr.BadRequest, "Malformed params"))}}
}

respOK, respConflict, err := a.upsertOperationalIntentReference(ctx, &req.Auth, req.Entityid, "", req.Body)
respOK, respConflict, err := a.upsertOperationalIntentReference(ctx, time.Now(), &req.Auth, req.Entityid, "", req.Body)
if err != nil {
err = stacktrace.Propagate(err, "Could not put Operational Intent Reference")
errResp := &restapi.ErrorResponse{Message: dsserr.Handle(ctx, err)}
Expand Down Expand Up @@ -351,7 +351,7 @@ func (a *Server) UpdateOperationalIntentReference(ctx context.Context, req *rest
Message: dsserr.Handle(ctx, stacktrace.PropagateWithCode(req.BodyParseError, dsserr.BadRequest, "Malformed params"))}}
}

respOK, respConflict, err := a.upsertOperationalIntentReference(ctx, &req.Auth, req.Entityid, req.Ovn, req.Body)
respOK, respConflict, err := a.upsertOperationalIntentReference(ctx, time.Now(), &req.Auth, req.Entityid, req.Ovn, req.Body)
if err != nil {
err = stacktrace.Propagate(err, "Could not put subscription")
errResp := &restapi.ErrorResponse{Message: dsserr.Handle(ctx, err)}
Expand All @@ -377,6 +377,7 @@ func (a *Server) UpdateOperationalIntentReference(ctx context.Context, req *rest
type validOIRParams struct {
id dssmodels.ID
ovn scdmodels.OVN
newOVN scdmodels.OVN
state scdmodels.OperationalIntentState
extents []*dssmodels.Volume4D
uExtent *dssmodels.Volume4D
Expand Down Expand Up @@ -404,7 +405,7 @@ func (vp *validOIRParams) toOIR(manager dssmodels.Manager, attachedSub *scdmodel
ID: vp.id,
Manager: manager,
Version: version,
OVN: "", // TODO dss#1078: this field must be populated to support USSs setting OVNs in advance
OVN: vp.newOVN, // non-empty only if the USS has requested an OVN
PastOVNs: pastOVNs,

StartTime: vp.uExtent.StartTime,
Expand All @@ -423,6 +424,7 @@ func (vp *validOIRParams) toOIR(manager dssmodels.Manager, attachedSub *scdmodel
// Note that this does NOT check for anything related to access controls: any error returned should be labeled
// as a dsserr.BadRequest.
func validateAndReturnUpsertParams(
now time.Time,
entityid restapi.EntityID,
ovn restapi.EntityOVN,
params *restapi.PutOperationalIntentReferenceParameters,
Expand Down Expand Up @@ -509,7 +511,7 @@ func validateAndReturnUpsertParams(
return nil, stacktrace.NewError("Missing time_end from extents")
}

if time.Now().After(*valid.uExtent.EndTime) {
if now.After(*valid.uExtent.EndTime) {
return nil, stacktrace.NewError("OperationalIntents may not end in the past")
}

Expand All @@ -531,6 +533,13 @@ func validateAndReturnUpsertParams(
}
valid.ovn = scdmodels.OVN(ovn)

if params.RequestedOvnSuffix != nil {
valid.newOVN, err = scdmodels.NewOVNFromUUIDv7Suffix(now, valid.id, string(*params.RequestedOvnSuffix))
if err != nil {
return nil, stacktrace.Propagate(err, "Invalid requested OVN suffix")
}
}

// Check if a subscription is required for this request:
// OIRs in an accepted state do not need a subscription.
if valid.state.RequiresSubscription() &&
Expand Down Expand Up @@ -798,11 +807,11 @@ func ensureSubscriptionCoversOIR(ctx context.Context, r repos.Repository, sub *s

// upsertOperationalIntentReference inserts or updates an Operational Intent.
// If the ovn argument is empty (""), it will attempt to create a new Operational Intent.
func (a *Server) upsertOperationalIntentReference(ctx context.Context, authorizedManager *api.AuthorizationResult, entityid restapi.EntityID, ovn restapi.EntityOVN, params *restapi.PutOperationalIntentReferenceParameters,
func (a *Server) upsertOperationalIntentReference(ctx context.Context, now time.Time, authorizedManager *api.AuthorizationResult, entityid restapi.EntityID, ovn restapi.EntityOVN, params *restapi.PutOperationalIntentReferenceParameters,
) (*restapi.ChangeOperationalIntentReferenceResponse, *restapi.AirspaceConflictResponse, error) {
// Note: validateAndReturnUpsertParams and checkUpsertPermissionsAndReturnManager could be moved out of this method and only the valid params passed,
// but this requires some changes in the caller that go beyond the immediate scope of #1088 and can be done later.
validParams, err := validateAndReturnUpsertParams(entityid, ovn, params, a.AllowHTTPBaseUrls)
validParams, err := validateAndReturnUpsertParams(now, entityid, ovn, params, a.AllowHTTPBaseUrls)
if err != nil {
return nil, nil, stacktrace.PropagateWithCode(err, dsserr.BadRequest, "Failed to validate Operational Intent Reference upsert parameters")
}
Expand Down

0 comments on commit 1606c4c

Please sign in to comment.