diff --git a/.gitmodules b/.gitmodules index 82b3e695f..1a330297b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -6,4 +6,7 @@ url = https://github.com/uastech/standards [submodule "interfaces/rid/v2"] path = interfaces/rid/v2 - url = https://github.com/uastech/standards \ No newline at end of file + url = https://github.com/uastech/standards +[submodule "interfaces/automated_testing_interfaces"] + path = interfaces/automated_testing_interfaces + url = https://github.com/interuss/automated_testing_interfaces.git diff --git a/build/dev/read_version.sh b/build/dev/read_version.sh new file mode 100755 index 000000000..d5f5f508d --- /dev/null +++ b/build/dev/read_version.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -eo pipefail + +# Retrieve token from dummy OAuth server +ACCESS_TOKEN=$(curl --silent \ + "http://localhost:8085/token?grant_type=client_credentials&scope=interuss.versioning.read_system_versions&intended_audience=localhost&issuer=localhost&sub=check_scd" \ +| python extract_json_field.py 'access_token') + +curl --silent -X GET \ +"http://localhost:8082/versions/local.test.identity" \ +-H "Authorization: Bearer ${ACCESS_TOKEN}" -H "Content-Type: application/json" + diff --git a/cmds/core-service/main.go b/cmds/core-service/main.go index e69017e67..572ed11e2 100644 --- a/cmds/core-service/main.go +++ b/cmds/core-service/main.go @@ -20,6 +20,7 @@ import ( apiridv1 "github.com/interuss/dss/pkg/api/ridv1" apiridv2 "github.com/interuss/dss/pkg/api/ridv2" apiscdv1 "github.com/interuss/dss/pkg/api/scdv1" + apiversioningv1 "github.com/interuss/dss/pkg/api/versioningv1" "github.com/interuss/dss/pkg/auth" aux "github.com/interuss/dss/pkg/aux_" "github.com/interuss/dss/pkg/build" @@ -33,6 +34,7 @@ import ( "github.com/interuss/dss/pkg/scd" scdc "github.com/interuss/dss/pkg/scd/store/cockroach" "github.com/interuss/dss/pkg/version" + "github.com/interuss/dss/pkg/versioning" "github.com/interuss/stacktrace" "github.com/robfig/cron/v3" "go.uber.org/zap" @@ -219,11 +221,12 @@ func RunHTTPServer(ctx context.Context, ctxCanceler func(), address, locality st } var ( - err error - ridV1Server *rid_v1.Server - ridV2Server *rid_v2.Server - scdV1Server *scd.Server - auxV1Server = &aux.Server{} + err error + ridV1Server *rid_v1.Server + ridV2Server *rid_v2.Server + scdV1Server *scd.Server + auxV1Server = &aux.Server{} + versioningV1Server = &versioning.Server{} ) // Initialize remote ID @@ -253,9 +256,16 @@ func RunHTTPServer(ctx context.Context, ctxCanceler func(), address, locality st } auxV1Router := apiauxv1.MakeAPIRouter(auxV1Server, authorizer) + versioningV1Router := apiversioningv1.MakeAPIRouter(versioningV1Server, authorizer) ridV1Router := apiridv1.MakeAPIRouter(ridV1Server, authorizer) ridV2Router := apiridv2.MakeAPIRouter(ridV2Server, authorizer) - multiRouter := api.MultiRouter{Routers: []api.PartialRouter{&auxV1Router, &ridV1Router, &ridV2Router}} + multiRouter := api.MultiRouter{ + Routers: []api.PartialRouter{ + &auxV1Router, + &versioningV1Router, + &ridV1Router, + &ridV2Router, + }} // Initialize strategic conflict detection if *enableSCD { diff --git a/interfaces/automated_testing_interfaces b/interfaces/automated_testing_interfaces new file mode 160000 index 000000000..4e07f46eb --- /dev/null +++ b/interfaces/automated_testing_interfaces @@ -0,0 +1 @@ +Subproject commit 4e07f46eb4452da761fb1658d3775d801d19312b diff --git a/pkg/api/versioningv1/interface.gen.go b/pkg/api/versioningv1/interface.gen.go new file mode 100644 index 000000000..eb050bc45 --- /dev/null +++ b/pkg/api/versioningv1/interface.gen.go @@ -0,0 +1,47 @@ +// This file is auto-generated; do not change as any changes will be overwritten +package versioning + +import ( + "context" + "github.com/interuss/dss/pkg/api" +) + +var ( + InterussVersioningReadSystemVersionsScope = api.RequiredScope("interuss.versioning.read_system_versions") + GetVersionSecurity = []api.AuthorizationOption{ + { + "Authority": {InterussVersioningReadSystemVersionsScope}, + }, + } +) + +type GetVersionRequest struct { + // The system identity/boundary for which a version should be provided, if known. + SystemIdentity SystemBoundaryIdentifier + + // The result of attempting to authorize this request + Auth api.AuthorizationResult +} +type GetVersionResponseSet struct { + // This interface successfully provided the version of the system identity/boundary that was requested. + Response200 *GetVersionResponse + + // Bearer access token was not provided in Authorization header, token could not be decoded, or token was invalid. + Response401 *api.EmptyResponseBody + + // The access token was decoded successfully but did not include a scope appropriate to this endpoint. + Response403 *api.EmptyResponseBody + + // The requested system identity/boundary is not known, or the versioning automated testing interface is not available. + Response404 *api.EmptyResponseBody + + // Auto-generated internal server error response + Response500 *api.InternalServerErrorBody +} + +type Implementation interface { + // System version + // --- + // Get the requested system version. + GetVersion(ctx context.Context, req *GetVersionRequest) GetVersionResponseSet +} diff --git a/pkg/api/versioningv1/server.gen.go b/pkg/api/versioningv1/server.gen.go new file mode 100644 index 000000000..219d91034 --- /dev/null +++ b/pkg/api/versioningv1/server.gen.go @@ -0,0 +1,74 @@ +// This file is auto-generated; do not change as any changes will be overwritten +package versioning + +import ( + "context" + "github.com/interuss/dss/pkg/api" + "net/http" + "regexp" +) + +type APIRouter struct { + Routes []*api.Route + Implementation Implementation + Authorizer api.Authorizer +} + +// *versioning.APIRouter (type defined above) implements the api.PartialRouter interface +func (s *APIRouter) Handle(w http.ResponseWriter, r *http.Request) bool { + for _, route := range s.Routes { + if route.Method == r.Method && route.Pattern.MatchString(r.URL.Path) { + route.Handler(route.Pattern, w, r) + return true + } + } + return false +} + +func (s *APIRouter) GetVersion(exp *regexp.Regexp, w http.ResponseWriter, r *http.Request) { + var req GetVersionRequest + + // Authorize request + req.Auth = s.Authorizer.Authorize(w, r, GetVersionSecurity) + + // Parse path parameters + pathMatch := exp.FindStringSubmatch(r.URL.Path) + req.SystemIdentity = SystemBoundaryIdentifier(pathMatch[1]) + + // Call implementation + ctx, cancel := context.WithCancel(r.Context()) + defer cancel() + response := s.Implementation.GetVersion(ctx, &req) + + // Write response to client + if response.Response200 != nil { + api.WriteJSON(w, 200, response.Response200) + return + } + if response.Response401 != nil { + api.WriteJSON(w, 401, response.Response401) + return + } + if response.Response403 != nil { + api.WriteJSON(w, 403, response.Response403) + return + } + if response.Response404 != nil { + api.WriteJSON(w, 404, response.Response404) + return + } + if response.Response500 != nil { + api.WriteJSON(w, 500, response.Response500) + return + } + api.WriteJSON(w, 500, api.InternalServerErrorBody{ErrorMessage: "Handler implementation did not set a response"}) +} + +func MakeAPIRouter(impl Implementation, auth api.Authorizer) APIRouter { + router := APIRouter{Implementation: impl, Authorizer: auth, Routes: make([]*api.Route, 1)} + + pattern := regexp.MustCompile("^/versions/(?P[^/]*)$") + router.Routes[0] = &api.Route{Method: http.MethodGet, Pattern: pattern, Handler: router.GetVersion} + + return router +} diff --git a/pkg/api/versioningv1/types.gen.go b/pkg/api/versioningv1/types.gen.go new file mode 100644 index 000000000..dd74ff269 --- /dev/null +++ b/pkg/api/versioningv1/types.gen.go @@ -0,0 +1,16 @@ +// This file is auto-generated; do not change as any changes will be overwritten +package versioning + +// Identifier of a system boundary, known to both the client and the USS separate from this API, for which this interface can provide a version. While the format is not prescribed by this API, any value must be URL-safe. It is recommended to use an approach similar to reverse-order Internet domain names and Java packages where the global scope is described with increasingly-precise identifiers joined by periods. For instance, the system boundary containing the mandatory Network Identification U-space service might be identified with `gov.eu.uspace.v1.netid` because the authority defining this system boundary is a governmental organization (specifically, the European Union) with requirements imposed on the system under test by the U-space regulation (first version) -- specifically, the Network Identification Service section. +type SystemBoundaryIdentifier string + +// Identifier of a particular version of a system (defined by a known system boundary). While the format is not prescribed by this API, a semantic version (https://semver.org/) prefixed with a `v` is recommended. +type VersionIdentifier string + +type GetVersionResponse struct { + // The requested system identity/boundary. + SystemIdentity *SystemBoundaryIdentifier `json:"system_identity,omitempty"` + + // The version of the system with the specified system identity/boundary. + SystemVersion *VersionIdentifier `json:"system_version,omitempty"` +} diff --git a/pkg/versioning/server.go b/pkg/versioning/server.go new file mode 100644 index 000000000..ec407ad93 --- /dev/null +++ b/pkg/versioning/server.go @@ -0,0 +1,44 @@ +package versioning + +import ( + "context" + "github.com/interuss/dss/pkg/api" + versioning "github.com/interuss/dss/pkg/api/versioningv1" + dsserr "github.com/interuss/dss/pkg/errors" + "github.com/interuss/dss/pkg/version" + "github.com/interuss/stacktrace" +) + +type Server struct { +} + +func (s *Server) GetVersion(ctx context.Context, req *versioning.GetVersionRequest) versioning.GetVersionResponseSet { + // This should take care of unauthenticated requests as well as + // any request without the proper scope. + if req.Auth.Error != nil { + resp := versioning.GetVersionResponseSet{} + setAuthError(ctx, stacktrace.Propagate(req.Auth.Error, "Auth failed"), &resp.Response401, &resp.Response403, &resp.Response500) + return resp + } + + // The DSS has no notion of particular system identities: whatever the request, we will + // always return the current version of the DSS binary. + versionStr := version.Current().String() + return versioning.GetVersionResponseSet{ + Response200: &versioning.GetVersionResponse{ + SystemIdentity: &req.SystemIdentity, + SystemVersion: (*versioning.VersionIdentifier)(&versionStr), + }, + } +} + +func setAuthError(ctx context.Context, authErr error, resp401, resp403 **api.EmptyResponseBody, resp500 **api.InternalServerErrorBody) { + switch stacktrace.GetCode(authErr) { + case dsserr.Unauthenticated: + *resp401 = &api.EmptyResponseBody{} + case dsserr.PermissionDenied: + *resp403 = &api.EmptyResponseBody{} + default: + *resp500 = &api.InternalServerErrorBody{ErrorMessage: *dsserr.Handle(ctx, stacktrace.Propagate(authErr, "Could not perform authorization"))} + } +} diff --git a/pkg/versioning/server_test.go b/pkg/versioning/server_test.go new file mode 100644 index 000000000..1099e9c22 --- /dev/null +++ b/pkg/versioning/server_test.go @@ -0,0 +1,31 @@ +package versioning + +import ( + "context" + versioning "github.com/interuss/dss/pkg/api/versioningv1" + "github.com/interuss/dss/pkg/version" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestServer_GetVersion(t *testing.T) { + s := &Server{} + + got := s.GetVersion(context.Background(), + &versioning.GetVersionRequest{ + SystemIdentity: "empty", + }).Response200 + + assert.Equal( + t, + version.Current().String(), + string(*got.SystemVersion), + ) + + assert.Equal( + t, + "empty", + string(*got.SystemIdentity), + ) + +}