Skip to content

Commit

Permalink
[dss][authorizations] Limit transition to off-nominal states to CMSA …
Browse files Browse the repository at this point in the history
…role (#1024)

* [dss][authorizations] Limit transition to off-nominal states to CMSA role

* Address PR comments
  • Loading branch information
barroco authored May 2, 2024
1 parent 89eefa0 commit 72367c9
Show file tree
Hide file tree
Showing 4 changed files with 48 additions and 14 deletions.
9 changes: 9 additions & 0 deletions pkg/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,15 @@ func (a *Authorizer) Authorize(_ http.ResponseWriter, r *http.Request, authOptio
}
}

func HasScope(scopes []string, requiredScope api.RequiredScope) bool {
for _, scope := range scopes {
if scope == string(requiredScope) {
return true
}
}
return false
}

// describeAuthorizationExpectations builds a human-readable string describing the expectations of the authorization options.
func describeAuthorizationExpectations(authOptions []api.AuthorizationOption) string {
if len(authOptions) == 0 {
Expand Down
12 changes: 12 additions & 0 deletions pkg/auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/golang-jwt/jwt"
"github.com/interuss/dss/pkg/api"
"github.com/interuss/dss/pkg/api/scdv1"
dsserr "github.com/interuss/dss/pkg/errors"
"github.com/interuss/stacktrace"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -220,3 +221,14 @@ func TestClaimsValidation(t *testing.T) {
claims.ExpiresAt = 45
require.Error(t, claims.Valid())
}

func TestHasScope(t *testing.T) {
scopes := []string{
string(scdv1.UtmStrategicCoordinationScope),
string(scdv1.UtmConformanceMonitoringSaScope),
}

require.True(t, HasScope(scopes, scdv1.UtmStrategicCoordinationScope))
require.True(t, HasScope(scopes, scdv1.UtmConformanceMonitoringSaScope))
require.False(t, HasScope(scopes, scdv1.UtmAvailabilityArbitrationScope))
}
11 changes: 11 additions & 0 deletions pkg/scd/models/operational_intents.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,17 @@ func (s OperationalIntentState) IsValidInDSS() bool {
return false
}

// RequiresCMSA indicates whether a state requires the CMSA role to be transition to.
func (s OperationalIntentState) RequiresCMSA() bool {
switch s {
case OperationalIntentStateNonconforming:
fallthrough
case OperationalIntentStateContingent:
return true
}
return false
}

// OperationalIntent models an operational intent.
type OperationalIntent struct {
// Reference
Expand Down
30 changes: 16 additions & 14 deletions pkg/scd/operational_intents_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/google/uuid"
"github.com/interuss/dss/pkg/api"
restapi "github.com/interuss/dss/pkg/api/scdv1"
"github.com/interuss/dss/pkg/auth"
dsserr "github.com/interuss/dss/pkg/errors"
dssmodels "github.com/interuss/dss/pkg/models"
scdmodels "github.com/interuss/dss/pkg/scd/models"
Expand Down Expand Up @@ -288,12 +289,8 @@ func (a *Server) CreateOperationalIntentReference(ctx context.Context, req *rest
return restapi.CreateOperationalIntentReferenceResponseSet{Response400: &restapi.ErrorResponse{
Message: dsserr.Handle(ctx, stacktrace.PropagateWithCode(req.BodyParseError, dsserr.BadRequest, "Malformed params"))}}
}
if req.Auth.ClientID == nil {
return restapi.CreateOperationalIntentReferenceResponseSet{Response403: &restapi.ErrorResponse{
Message: dsserr.Handle(ctx, stacktrace.NewErrorWithCode(dsserr.PermissionDenied, "Missing manager"))}}
}

respOK, respConflict, err := a.PutOperationalIntentReference(ctx, *req.Auth.ClientID, req.Entityid, "", req.Body)
respOK, respConflict, err := a.upsertOperationalIntentReference(ctx, &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 @@ -328,12 +325,8 @@ func (a *Server) UpdateOperationalIntentReference(ctx context.Context, req *rest
return restapi.UpdateOperationalIntentReferenceResponseSet{Response400: &restapi.ErrorResponse{
Message: dsserr.Handle(ctx, stacktrace.PropagateWithCode(req.BodyParseError, dsserr.BadRequest, "Malformed params"))}}
}
if req.Auth.ClientID == nil {
return restapi.UpdateOperationalIntentReferenceResponseSet{Response403: &restapi.ErrorResponse{
Message: dsserr.Handle(ctx, stacktrace.NewErrorWithCode(dsserr.PermissionDenied, "Missing manager"))}}
}

respOK, respConflict, err := a.PutOperationalIntentReference(ctx, *req.Auth.ClientID, req.Entityid, req.Ovn, req.Body)
respOK, respConflict, err := a.upsertOperationalIntentReference(ctx, &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 @@ -356,10 +349,15 @@ func (a *Server) UpdateOperationalIntentReference(ctx context.Context, req *rest
return restapi.UpdateOperationalIntentReferenceResponseSet{Response200: respOK}
}

// PutOperationalIntentReference inserts or updates an Operational Intent.
// 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) PutOperationalIntentReference(ctx context.Context, manager string, entityid restapi.EntityID, ovn restapi.EntityOVN, params *restapi.PutOperationalIntentReferenceParameters,
func (a *Server) upsertOperationalIntentReference(ctx context.Context, authorizedManager *api.AuthorizationResult, entityid restapi.EntityID, ovn restapi.EntityOVN, params *restapi.PutOperationalIntentReferenceParameters,
) (*restapi.ChangeOperationalIntentReferenceResponse, *restapi.AirspaceConflictResponse, error) {
if authorizedManager.ClientID == nil {
return nil, nil, stacktrace.NewErrorWithCode(dsserr.PermissionDenied, "Missing manager")
}
manager := dssmodels.Manager(*authorizedManager.ClientID)

id, err := dssmodels.IDFromString(string(entityid))
if err != nil {
return nil, nil, stacktrace.NewErrorWithCode(dsserr.BadRequest, "Invalid ID format: `%s`", entityid)
Expand All @@ -384,6 +382,10 @@ func (a *Server) PutOperationalIntentReference(ctx context.Context, manager stri
if !state.IsValidInDSS() {
return nil, nil, stacktrace.NewErrorWithCode(dsserr.BadRequest, "Invalid OperationalIntent state: %s", params.State)
}
hasCMSARole := auth.HasScope(authorizedManager.Scopes, restapi.UtmConformanceMonitoringSaScope)
if state.RequiresCMSA() && !hasCMSARole {
return nil, nil, stacktrace.NewErrorWithCode(dsserr.PermissionDenied, "Missing `%s` Conformance Monitoring for Situational Awareness scope to transition to CMSA state: %s (see SCD0100)", restapi.UtmConformanceMonitoringSaScope, params.State)
}

for idx, extent := range params.Extents {
cExtent, err := dssmodels.Volume4DFromSCDRest(&extent)
Expand Down Expand Up @@ -456,7 +458,7 @@ func (a *Server) PutOperationalIntentReference(ctx context.Context, manager stri
return stacktrace.Propagate(err, "Could not get OperationalIntent from repo")
}
if old != nil {
if old.Manager != dssmodels.Manager(manager) {
if old.Manager != manager {
return stacktrace.NewErrorWithCode(dsserr.PermissionDenied,
"OperationalIntent owned by %s, but %s attempted to modify", old.Manager, manager)
}
Expand Down Expand Up @@ -490,7 +492,7 @@ func (a *Server) PutOperationalIntentReference(ctx context.Context, manager stri

subToUpsert := scdmodels.Subscription{
ID: dssmodels.ID(uuid.New().String()),
Manager: dssmodels.Manager(manager),
Manager: manager,
StartTime: uExtent.StartTime,
EndTime: uExtent.EndTime,
AltitudeLo: uExtent.SpatialVolume.AltitudeLo,
Expand Down

0 comments on commit 72367c9

Please sign in to comment.