diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index 41f4dc04f..3847a3b41 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -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 { diff --git a/pkg/auth/auth_test.go b/pkg/auth/auth_test.go index 19dccaecd..8461b7157 100644 --- a/pkg/auth/auth_test.go +++ b/pkg/auth/auth_test.go @@ -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" @@ -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)) +} diff --git a/pkg/scd/models/operational_intents.go b/pkg/scd/models/operational_intents.go index 45771d1b3..6e5d607a3 100644 --- a/pkg/scd/models/operational_intents.go +++ b/pkg/scd/models/operational_intents.go @@ -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 diff --git a/pkg/scd/operational_intents_handler.go b/pkg/scd/operational_intents_handler.go index f00c1e72c..919b0d7ff 100644 --- a/pkg/scd/operational_intents_handler.go +++ b/pkg/scd/operational_intents_handler.go @@ -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" @@ -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)} @@ -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)} @@ -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) @@ -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) @@ -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) } @@ -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,