Skip to content

Commit 37488ab

Browse files
authored
Merge pull request #294 from USACE/chore/survey123-telemetry-proxy
chore(api, telemetry): Update Survey123 to use telemetry proxy
2 parents 1e22dff + 991a87a commit 37488ab

File tree

16 files changed

+275
-48
lines changed

16 files changed

+275
-48
lines changed

api/internal/config/api.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package config
22

33
import (
4-
"net/url"
54
"os"
65
"time"
76

@@ -22,7 +21,6 @@ type ApiConfig struct {
2221
AlertEventBatchSize int `env:"ALERT_EVENT_BATCH_SIZE" envDefault:"100"`
2322
AlertEventBatchTimeout time.Duration `env:"ALERT_EVENT_TIMEOUT" envDefault:"30s"`
2423
AlertEventFlushWorkers int `env:"ALERT_EVENT_FLUSH_WORKERS" envDefault:"4"`
25-
Survey123IPWhitelist []url.URL `env:"SURVEY123_IP_WHITELIST"`
2624
IrisFdsnProxyURL string `env:"IRIS_FDSN_PROXY_URL" envDefault:"https://service.iris.edu/fdsnws/station/1/query"`
2725
CwmsProxyURL string `env:"CWMS_PROXY_URL" envDefault:"https://cwms-data.usace.army.mil/cwms-data/"`
2826
}

api/internal/db/models.go

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/internal/db/overrides.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,6 @@ type SaaMeasurement struct {
171171
type Survey123EquivalencyTableField struct {
172172
FieldName string `json:"field_name"`
173173
DisplayName string `json:"display_name"`
174-
InstrumentID *uuid.UUID `json:"instrument_id"`
175174
TimeseriesID *uuid.UUID `json:"timeseries_id"`
176175
}
177176

api/internal/db/querier.go

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/internal/db/survey123.sql_gen.go

Lines changed: 39 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/internal/dto/survey123.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package dto
22

33
import (
4+
"encoding/json"
5+
46
"github.com/google/uuid"
57
)
68

@@ -33,3 +35,8 @@ type Survey123ApplyEditsDTO struct {
3335
ObjectID any `json:"objectId,omitempty" required:"false"`
3436
Geometry any `json:"geometry,omitempty" required:"false"`
3537
}
38+
39+
type Survey123TelemetryDTO struct {
40+
Key string
41+
Payload map[string]json.RawMessage
42+
}

api/internal/handler/api.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,7 @@ type apiMiddlewares struct {
134134
ProjectAdmin,
135135
ProjectMember,
136136
AppAdmin,
137-
InternalApp,
138-
Survey123IPWhitelist huma.Middlewares
137+
InternalApp huma.Middlewares
139138
}
140139

141140
type security []map[string][]string
@@ -202,7 +201,6 @@ func NewApiRouter(ctx context.Context, h *ApiHandler) *Router {
202201
h.ProjectMember = append(h.ProjectMember, mw.IsProjectMember)
203202
h.Cac = huma.Middlewares{mw.JWT, mw.AttachClaims, mw.RequireClaims}
204203
h.InternalApp = huma.Middlewares{mw.AppKeyAuth}
205-
h.Survey123IPWhitelist = huma.Middlewares{mw.NewWhitelistXFFMiddleware(h.Config.Survey123IPWhitelist)}
206204

207205
humaCfg := huma.DefaultConfig("MIDAS API", API_VERSION)
208206
humaCfg.Servers = append(humaCfg.Servers, &huma.Server{URL: h.Config.ServerBaseUrl + API_VERSION_PREFIX})

api/internal/handler/survey123.go

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,18 @@ package handler
22

33
import (
44
"context"
5+
"database/sql"
56
"encoding/json"
7+
"errors"
68
"net/http"
79
"time"
810

911
"github.com/USACE/instrumentation-api/api/v4/internal/ctxkey"
1012
"github.com/USACE/instrumentation-api/api/v4/internal/db"
1113
"github.com/USACE/instrumentation-api/api/v4/internal/dto"
1214
"github.com/USACE/instrumentation-api/api/v4/internal/httperr"
15+
"github.com/USACE/instrumentation-api/api/v4/internal/password"
16+
"github.com/USACE/instrumentation-api/api/v4/internal/service"
1317
"github.com/danielgtaylor/huma/v2"
1418
)
1519

@@ -50,6 +54,9 @@ func (h *ApiHandler) RegisterSurvey123(api huma.API) {
5054
}) (*Response[db.Survey123Preview], error) {
5155
a, err := h.DBService.Survey123PreviewGet(ctx, input.Survey123ID.UUID)
5256
if err != nil {
57+
if errors.Is(err, sql.ErrNoRows) {
58+
return nil, httperr.NotFound(err)
59+
}
5360
return nil, httperr.InternalServerError(err)
5461
}
5562
return NewResponse(a), nil
@@ -67,7 +74,7 @@ func (h *ApiHandler) RegisterSurvey123(api huma.API) {
6774
}, func(ctx context.Context, input *struct {
6875
ProjectIDParam
6976
Body dto.Survey123DTO
70-
}) (*Response[ID], error) {
77+
}) (*Response[service.Survey123IDWithKey], error) {
7178
sv := input.Body
7279
sv.ProjectID = input.ProjectID.UUID
7380

@@ -82,7 +89,34 @@ func (h *ApiHandler) RegisterSurvey123(api huma.API) {
8289
return nil, httperr.InternalServerError(err)
8390
}
8491

85-
return NewResponseID(a), nil
92+
return NewResponse(a), nil
93+
})
94+
95+
huma.Register(api, huma.Operation{
96+
Middlewares: h.ProjectAdmin,
97+
Security: projectAdminSecurity,
98+
OperationID: "survey123-update-key",
99+
Method: http.MethodPut,
100+
Path: "/projects/{project_id}/survey123/{survey123_id}/key",
101+
Description: "cycles a Survey123 key (project admin)",
102+
Tags: survey123Tags,
103+
}, func(ctx context.Context, input *struct {
104+
ProjectIDParam
105+
Survey123IDParam
106+
}) (*Response[service.Survey123IDWithKey], error) {
107+
key := password.GenerateRandom(40)
108+
hash := password.MustCreateHash(key, password.DefaultParams)
109+
110+
if err := h.DBService.Survey123HashUpdate(ctx, db.Survey123HashUpdateParams{
111+
Survey123ID: input.Survey123ID.UUID,
112+
Hash: hash,
113+
}); err != nil {
114+
return nil, httperr.InternalServerError(err)
115+
}
116+
return NewResponse(service.Survey123IDWithKey{
117+
ID: input.Survey123ID.UUID,
118+
Key: key,
119+
}), nil
86120
})
87121

88122
huma.Register(api, huma.Operation{
@@ -139,22 +173,32 @@ func (h *ApiHandler) RegisterSurvey123(api huma.API) {
139173

140174
huma.Register(api, huma.Operation{
141175
Hidden: true,
142-
Middlewares: h.Survey123IPWhitelist,
176+
Middlewares: h.InternalApp,
143177
OperationID: "survey123-telemetry-create-or-update-measurements",
144178
Method: http.MethodPost,
145179
Path: "/webhooks/survey123/{survey123_id}/measurements",
146180
Description: "webhook for creating or updating measurements for survey123 mappings from AGS04",
147181
Tags: survey123Tags,
148182
}, func(ctx context.Context, input *struct {
149183
Survey123IDParam
150-
Body map[string]json.RawMessage
151-
}) (*Response[ID], error) {
184+
Body dto.Survey123TelemetryDTO
185+
}) (*struct{}, error) {
186+
hash, err := h.DBService.Survey123HashGet(ctx, input.Survey123ID.UUID)
187+
if err != nil {
188+
if errors.Is(err, sql.ErrNoRows) {
189+
return nil, httperr.Unauthorized(err)
190+
}
191+
return nil, httperr.InternalServerError(err)
192+
}
193+
match, err := password.ComparePasswordAndHash(input.Body.Key, hash)
194+
if err != nil || !match {
195+
return nil, httperr.Unauthorized(errors.New("error validating survey123 hash"))
196+
}
152197

153-
preview, err := json.Marshal(input.Body)
198+
preview, err := json.Marshal(input.Body.Payload)
154199
if err != nil {
155200
return nil, httperr.BadRequest(err)
156201
}
157-
158202
if err := h.DBService.Survey123PreviewCreateOrUpdate(ctx, db.Survey123PreviewCreateOrUpdateParams{
159203
Survey123ID: input.Survey123ID.UUID,
160204
Preview: string(preview),
@@ -168,7 +212,7 @@ func (h *ApiHandler) RegisterSurvey123(api huma.API) {
168212
return nil, httperr.ServerErrorOrNotFound(err)
169213
}
170214

171-
raw := input.Body
215+
raw := input.Body.Payload
172216
var et string
173217
if err := json.Unmarshal(raw["eventType"], &et); err != nil {
174218
return nil, httperr.UnprocessableEntity(err)
@@ -185,6 +229,6 @@ func (h *ApiHandler) RegisterSurvey123(api huma.API) {
185229
return nil, httperr.InternalServerError(err)
186230
}
187231

188-
return NewResponseID(input.Survey123ID.UUID), nil
232+
return nil, nil
189233
})
190234
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package handler
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"errors"
8+
"fmt"
9+
"io"
10+
"net/http"
11+
"net/url"
12+
13+
"github.com/USACE/instrumentation-api/api/v4/internal/dto"
14+
"github.com/USACE/instrumentation-api/api/v4/internal/httperr"
15+
"github.com/USACE/instrumentation-api/api/v4/internal/util"
16+
"github.com/danielgtaylor/huma/v2"
17+
)
18+
19+
func (h *TelemetryHandler) RegisterSurvey123Proxy(api huma.API) {
20+
huma.Register(api, huma.Operation{
21+
Hidden: true,
22+
Path: "/telemetry/survey123/{survey123_id}",
23+
OperationID: "survey123-telemetry-proxy",
24+
Method: http.MethodPost,
25+
Description: "survey123 webhook telemetry ingress",
26+
MaxBodyBytes: maxBodyBytes,
27+
}, func(_ context.Context, input *struct {
28+
Key string `query:"key"`
29+
Survey123IDParam
30+
RawBody map[string]json.RawMessage
31+
}) (*ResponseWithStatus[any], error) {
32+
ctx := context.Background()
33+
body, err := json.Marshal(dto.Survey123TelemetryDTO{
34+
Key: input.Key,
35+
Payload: input.RawBody,
36+
})
37+
if err != nil {
38+
return nil, httperr.BadRequest(err)
39+
}
40+
req, err := http.NewRequestWithContext(
41+
ctx,
42+
http.MethodPost,
43+
fmt.Sprintf("%s/webhooks/survey123/%s/measurements?key=%s", h.Config.ApiHost, input.Survey123IDParam, h.Config.ApplicationKey),
44+
bytes.NewReader(body),
45+
)
46+
if err != nil {
47+
return nil, httperr.BadRequest(errors.New("could not construct request with given parameters"))
48+
}
49+
defer func() {
50+
if err := req.Body.Close(); err != nil {
51+
h.Logger.Error(ctx, "error closing request body", "error", err)
52+
}
53+
}()
54+
55+
res, err := h.Client.Do(req)
56+
if err != nil {
57+
var urlErr *url.Error
58+
if errors.As(err, &urlErr) {
59+
u, err := url.Parse(urlErr.URL)
60+
if err != nil {
61+
return nil, httperr.InternalServerError(errors.New("error occured while attempting to parse the url of another error"))
62+
}
63+
util.RedactQueryParams(u, "key")
64+
urlErr.URL = u.String()
65+
return nil, httperr.InternalServerError(fmt.Errorf("urlErr %w", urlErr))
66+
}
67+
return nil, httperr.InternalServerError(fmt.Errorf("expected error to be *url.Error type, got %T", err))
68+
}
69+
defer func() {
70+
if err := res.Body.Close(); err != nil {
71+
h.Logger.Error(ctx, "error closing response body", "error", err)
72+
}
73+
}()
74+
resBody, err := io.ReadAll(res.Body)
75+
if err != nil {
76+
return nil, httperr.InternalServerError(fmt.Errorf("failed to read response body %w", err))
77+
}
78+
79+
h.Logger.Debug(ctx, "received response from api server", "code", res.StatusCode, "response_body", json.RawMessage(resBody))
80+
81+
switch res.StatusCode {
82+
case http.StatusCreated:
83+
return NewResponseWithStatus(http.StatusCreated, any(resBody)), nil
84+
case http.StatusNotFound:
85+
// Issue with upstream server
86+
return nil, httperr.Message(http.StatusBadGateway, "invalid response from upstream proxy")
87+
default:
88+
return NewResponseWithStatus(http.StatusInternalServerError, any(json.RawMessage(resBody))), nil
89+
}
90+
91+
})
92+
}

api/internal/handler/telemetry.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,16 @@ func newTelemetryRouter(h *TelemetryHandler) *Router {
7676
api := humaecho.NewWithGroup(e, g, humaCfg)
7777

7878
humaCfg.Components.SecuritySchemes = map[string]*huma.SecurityScheme{
79-
keyAuth: {
79+
"apiKey-header": {
8080
Name: "X-Api-Key",
8181
Type: "apiKey",
8282
In: "header",
8383
},
84+
"apiKey-query": {
85+
Name: "key",
86+
Type: "apiKey",
87+
In: "query",
88+
},
8489
}
8590

8691
huma.AutoRegister(api, h)

0 commit comments

Comments
 (0)