Skip to content

Commit bb79ae7

Browse files
committed
Halfway through webhooks refactor
1 parent bf79b9c commit bb79ae7

4 files changed

Lines changed: 95 additions & 111 deletions

File tree

internal/config/config.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,10 +261,24 @@ const (
261261
DeleteUser WebhookActionType = "delete_user"
262262
)
263263

264+
type Webhook struct {
265+
ID string `json:"id" validate:"required"`
266+
Action WebhookActionType `json:"action" validate:"required,oneof=create_token delete_device delete_user"`
267+
JsonAttributeRoles WebhookAttributeMapping `json:"json_attribute_roles" validate:"required"`
268+
}
269+
270+
type WebhookAttributeMapping struct {
271+
AsUsername string `json:"as_username" validate:"omitempty,max=255,min=1"`
272+
AsDeviceTag string `json:"as_device_tag" validate:"omitempty,max=255,min=1"`
273+
AsRegistrationToken string `json:"as_registration_token" validate:"omitempty,max=255,min=1"`
274+
AsDeviceIP string `json:"as_device_ip" validate:"omitempty,max=255,min=1"`
275+
}
276+
264277
type Webhooks struct {
278+
// webhook id -> auth string
265279
Auth map[string]string
266-
Temporary map[string]any
267-
Active map[string]any
280+
Temporary map[string]Webhook
281+
Active map[string]Webhook
268282

269283
LastRequests LastRequests
270284
}

internal/data/config_internal_etcd.go

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

internal/data/webhooks.go

Lines changed: 73 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -4,52 +4,39 @@ import (
44
"context"
55
"crypto/pbkdf2"
66
"crypto/sha256"
7+
"crypto/subtle"
78
"encoding/hex"
89
"encoding/json"
910
"fmt"
10-
"path"
1111
"time"
1212

1313
"github.com/rs/zerolog/log"
1414

15+
"github.com/NHAS/tetcd"
16+
"github.com/NHAS/wag/internal/config"
1517
"github.com/NHAS/wag/internal/utils"
1618
"github.com/go-playground/validator/v10"
1719
clientv3 "go.etcd.io/etcd/client/v3"
1820
"go.etcd.io/etcd/client/v3/clientv3util"
1921
)
2022

21-
func (d *database) GetWebhookAuthPath(id, plainTextCredentials string) (string, error) {
23+
func (d *database) generateWebhookSecret(id, plainTextCredentials string) (string, error) {
2224

2325
res, err := pbkdf2.Key(sha256.New, plainTextCredentials, []byte(id), 10, 32)
2426
if err != nil {
2527
return "", fmt.Errorf("unable to determine hash: %w", err)
2628
}
2729

28-
result := path.Join(WebhookAuthPrefix, id, hex.EncodeToString(res))
29-
30-
return result, nil
31-
}
32-
33-
type Webhook struct {
34-
ID string `json:"id" validate:"required"`
35-
Action string `json:"action" validate:"required,oneof=create_token delete_device delete_user"`
36-
JsonAttributeRoles WebhookAttributeMapping `json:"json_attribute_roles" validate:"required"`
37-
}
38-
39-
type WebhookAttributeMapping struct {
40-
AsUsername string `json:"as_username" validate:"omitempty,max=255,min=1"`
41-
AsDeviceTag string `json:"as_device_tag" validate:"omitempty,max=255,min=1"`
42-
AsRegistrationToken string `json:"as_registration_token" validate:"omitempty,max=255,min=1"`
43-
AsDeviceIP string `json:"as_device_ip" validate:"omitempty,max=255,min=1"`
30+
return hex.EncodeToString(res), nil
4431
}
4532

4633
type WebhookCreateRequestDTO struct {
47-
Webhook
34+
config.Webhook
4835
AuthHeader string `json:"auth_header,omitempty" validate:"required,min=32,max=32"`
4936
}
5037

5138
type WebhookGetResponseDTO struct {
52-
Webhook
39+
config.Webhook
5340
LastRequestTime time.Time `json:"time"`
5441
LastRequestStatus string `json:"status"`
5542
}
@@ -63,33 +50,24 @@ func (d *database) GetWebhookLastRequest(id string) (string, error) {
6350
return InternalConfig.Webhooks.LastRequests.Data().Key(id).Get(context.Background(), d.etcd)
6451
}
6552

66-
func (d *database) GetWebhook(id string) (WebhookGetResponseDTO, error) {
67-
68-
return Get[WebhookGetResponseDTO](d.etcd, ActiveWebhooksPrefix+id)
69-
}
70-
7153
func (d *database) GetWebhooks() (hooks []WebhookGetResponseDTO, err error) {
7254

73-
response, err := d.etcd.Get(context.Background(), ActiveWebhooksPrefix, clientv3.WithPrefix(), clientv3.WithSort(clientv3.SortByKey, clientv3.SortDescend))
55+
order, data, err := InternalConfig.Webhooks.Active().List(context.Background(), d.etcd, clientv3.WithSort(clientv3.SortByKey, clientv3.SortDescend))
7456
if err != nil {
7557
return nil, err
7658
}
7759

7860
// otherwise json returns null
79-
hooks = []WebhookGetResponseDTO{}
80-
lastRequestOps := []clientv3.Op{}
81-
lastRequestStatusOps := []clientv3.Op{}
82-
for _, res := range response.Kvs {
83-
var hook WebhookGetResponseDTO
84-
err := json.Unmarshal(res.Value, &hook)
85-
if err != nil {
86-
return nil, err
87-
}
61+
hooks = make([]WebhookGetResponseDTO, 0, len(data))
8862

89-
lastRequestOps = append(lastRequestOps, clientv3.OpGet(d.GetLastWebhookRequestPath(hook.ID, "time"), clientv3.WithRev(response.Header.Revision)))
90-
lastRequestStatusOps = append(lastRequestStatusOps, clientv3.OpGet(d.GetLastWebhookRequestPath(hook.ID, "status"), clientv3.WithRev(response.Header.Revision)))
63+
txn := tetcd.NewTxn(context.Background(), d.etcd)
64+
then := txn.Then()
9165

92-
hooks = append(hooks, hook)
66+
for _, id := range order {
67+
hooks = append(hooks, WebhookGetResponseDTO{Webhook: data[id]})
68+
69+
tetcd.GetTx(then, InternalConfig.Webhooks.LastRequests.Time().Key(id), clientv3.WithRev(response.Header.Revision))
70+
tetcd.GetTx(then, InternalConfig.Webhooks.LastRequests.Status().Key(id), clientv3.WithRev(response.Header.Revision))
9371
}
9472

9573
resp, err := d.etcd.Txn(context.Background()).Then(lastRequestOps...).Commit()
@@ -147,17 +125,17 @@ func (d *database) CheckWebhookAuth(id, authHeader string) bool {
147125
return false
148126
}
149127

150-
path, err := d.GetWebhookAuthPath(id, authHeader)
128+
key, err := d.generateWebhookSecret(id, authHeader)
151129
if err != nil {
152130
return false
153131
}
154132

155-
resp, err := d.etcd.Get(context.Background(), path)
133+
resp, err := InternalConfig.Webhooks.Auth().Key(id).Get(context.Background(), d.etcd)
156134
if err != nil {
157135
return false
158136
}
159137

160-
return len(resp.Kvs) == 1
138+
return subtle.ConstantTimeCompare([]byte(resp), []byte(key)) == 1
161139
}
162140

163141
func (d *database) WebhookRecordLastRequest(id, authHeader, request string) error {
@@ -169,54 +147,42 @@ func (d *database) WebhookRecordLastRequest(id, authHeader, request string) erro
169147
return fmt.Errorf("storing webhook request encountered an error, input was too big >4096 bytes")
170148
}
171149

172-
requestBytes, _ := json.Marshal(request)
173-
174-
timeBytes, _ := json.Marshal(time.Now())
175-
176-
res, err := d.etcd.Txn(context.Background()).If(
177-
clientv3util.KeyExists(ActiveWebhooksPrefix+id),
178-
).Then(
179-
clientv3.OpGet(ActiveWebhooksPrefix+id),
180-
clientv3.OpPut(d.GetLastWebhookRequestPath(id, "data"), string(requestBytes)),
181-
clientv3.OpPut(d.GetLastWebhookRequestPath(id, "time"), string(timeBytes)),
182-
).Else(
183-
clientv3.OpTxn(
184-
[]clientv3.Cmp{
185-
clientv3util.KeyExists(TempWebhooksPrefix + id),
186-
},
187-
[]clientv3.Op{
188-
clientv3.OpPut(d.GetLastWebhookRequestPath(id, "data"), string(requestBytes)),
189-
},
190-
nil,
191-
),
192-
).Commit()
193-
194-
if res.Succeeded {
195-
196-
if len(res.Responses) != 3 {
197-
return fmt.Errorf("unable read response incorrect size: %d", len(res.Responses))
198-
}
150+
txn := tetcd.NewTxn(context.Background(), d.etcd)
151+
then, elseHandle := txn.Conditional(clientv3util.KeyExists(InternalConfig.Webhooks.Active().Key(id).Key()))
199152

200-
if len(res.Responses[0].GetResponseRange().Kvs) != 1 {
201-
return fmt.Errorf("incorrect key value size for getting webhook action: %q", id)
202-
}
153+
activeHandle := tetcd.GetTx(then, InternalConfig.Webhooks.Active().Key(id))
154+
tetcd.PutTx(then, InternalConfig.Webhooks.LastRequests.Data().Key(id), request)
155+
tetcd.PutTx(then, InternalConfig.Webhooks.LastRequests.Time().Key(id), time.Now())
203156

204-
var hookSettings Webhook
205-
err = json.Unmarshal(res.Responses[0].GetResponseRange().Kvs[0].Value, &hookSettings)
157+
failureTxn := tetcd.SubTx(elseHandle)
158+
failureThen, _ := failureTxn.Conditional(clientv3util.KeyExists(InternalConfig.Webhooks.Temporary().Key(id).Key()))
159+
160+
tetcd.PutTx(failureThen, InternalConfig.Webhooks.LastRequests.Data().Key(id), request)
161+
162+
if err := txn.Commit(); err != nil {
163+
return fmt.Errorf("failed to commit transaction: %w", err)
164+
}
165+
166+
if txn.Succeeded() {
167+
168+
hookSettings, err := activeHandle.Value()
206169
if err != nil {
207-
return fmt.Errorf("unable to unmarshal webhook settings: %w", err)
170+
return fmt.Errorf("failed to unmarshal webhook settings: %w", err)
208171
}
209172

210173
go d.actionWebhook(hookSettings, &request)
174+
return nil
175+
176+
}
211177

212-
} else if !res.Responses[0].GetResponseTxn().Succeeded {
178+
if !failureTxn.Succeeded() {
213179
return fmt.Errorf("webhook not found")
214180
}
215181

216-
return err
182+
return nil
217183
}
218184

219-
func (d *database) actionWebhook(hook Webhook, request *string) {
185+
func (d *database) actionWebhook(hook config.Webhook, request *string) {
220186

221187
var c map[string]any
222188

@@ -262,30 +228,31 @@ func (d *database) actionWebhook(hook Webhook, request *string) {
262228

263229
switch hook.Action {
264230

265-
case CreateRegistrationToken:
231+
case config.CreateRegistrationToken:
266232

267233
err = d.AddRegistrationToken(Token, Username, "", "", nil, 1, DeviceTag)
268234

269-
case DeleteDevice:
235+
case config.DeleteDevice:
270236
if DeviceIP != "" {
271237
err = d.DeleteDevice(DeviceIP)
272238
} else {
273239
err = d.DeleteDeviceByTag(DeviceTag)
274240
}
275241

276-
case DeleteUser:
242+
case config.DeleteUser:
277243
err = d.DeleteUser(Username)
278244
}
279245

280246
status := "OK"
281247
if err != nil {
282248

283249
status = err.Error()
284-
log.Error().Err(err).Str("action", hook.Action).Str("hook_id", hook.ID).Msg("failed to action webhook")
250+
log.Error().Err(err).Str("action", string(hook.Action)).Str("hook_id", hook.ID).Msg("failed to action webhook")
285251
d.RaiseError(fmt.Errorf("unable to do %q via webhook %q as error occured: %w", hook.Action, hook.ID, err), nil)
286252
}
287253

288-
Set(d.etcd, d.GetLastWebhookRequestPath(hook.ID, "status"), true, status)
254+
InternalConfig.Webhooks.LastRequests.Status().Key(hook.ID).Put(context.Background(),
255+
d.etcd, status)
289256
}
290257

291258
func (d *database) CreateWebhook(webhook WebhookCreateRequestDTO) error {
@@ -295,20 +262,19 @@ func (d *database) CreateWebhook(webhook WebhookCreateRequestDTO) error {
295262
return fmt.Errorf("validation of new webhook failed: %w", err)
296263
}
297264

298-
credPath, err := d.GetWebhookAuthPath(webhook.ID, webhook.AuthHeader)
265+
secret, err := d.generateWebhookSecret(webhook.ID, webhook.AuthHeader)
299266
if err != nil {
300267
return fmt.Errorf("could not store auth materical for web hook: %w", err)
301268
}
302269

303-
b, _ := json.Marshal(webhook)
270+
txn := tetcd.NewTxn(context.Background(), d.etcd)
271+
then := txn.Then()
272+
tetcd.DeleteTx(then, InternalConfig.Webhooks.Temporary().Key(webhook.ID), clientv3.WithPrefix())
273+
tetcd.PutTx(then, InternalConfig.Webhooks.Auth().Key(webhook.ID), secret) // clears the lease that was created for the temporary webhook
274+
tetcd.PutTx(then, InternalConfig.Webhooks.Active().Key(webhook.ID), webhook.Webhook)
304275

305-
_, err = d.etcd.Txn(context.Background()).Then(
306-
clientv3.OpDelete(TempWebhooksPrefix+webhook.ID, clientv3.WithPrefix()),
307-
clientv3.OpPut(credPath, "\"\""), // this clears the lease (hopefully)
308-
clientv3.OpPut(ActiveWebhooksPrefix+webhook.ID, string(b)),
309-
).Commit()
276+
return txn.Commit()
310277

311-
return err
312278
}
313279

314280
func (d *database) CreateTempWebhook() (string, string, error) {
@@ -329,31 +295,36 @@ func (d *database) CreateTempWebhook() (string, string, error) {
329295
return "", "", fmt.Errorf("failed to generate auth header: %w", err)
330296
}
331297

332-
authPath, err := d.GetWebhookAuthPath(temp.ID, authHeader)
298+
secret, err := d.generateWebhookSecret(temp.ID, authHeader)
333299
if err != nil {
334300
return "", "", fmt.Errorf("could not use generated value as auth header: %w", err)
335301
}
336302

337-
tempBytes, _ := json.Marshal(temp)
303+
txn := tetcd.NewTxn(context.Background(), d.etcd)
304+
then := txn.Then()
338305

339-
_, err = d.etcd.Txn(context.Background()).Then(
340-
clientv3.OpPut(TempWebhooksPrefix+temp.ID, string(tempBytes), clientv3.WithLease(lease.ID)),
341-
clientv3.OpPut(authPath, "\"\"", clientv3.WithLease(lease.ID)),
342-
).Commit()
306+
tetcd.PutTx(then, InternalConfig.Webhooks.Temporary().Key(temp.ID), temp.Webhook, clientv3.WithLease(lease.ID))
307+
tetcd.PutTx(then, InternalConfig.Webhooks.Auth().Key(temp.ID), secret, clientv3.WithLease(lease.ID))
308+
309+
if err = txn.Commit(); err != nil {
310+
return "", "", fmt.Errorf("failed to commit transaction for temp webhook: %w", err)
311+
}
343312

344313
return temp.ID, authHeader, err
345314
}
346315

347-
func (d *database) DeleteWebhooks(ids []string) error {
316+
func (d *database) DeleteWebhooks(ids []string, txn *tetcd.TxnConditional) error {
348317

349318
var ops []clientv3.Op
350319

351320
for _, id := range ids {
352-
ops = append(ops,
353-
clientv3.OpDelete(ActiveWebhooksPrefix+id, clientv3.WithPrefix()),
354-
clientv3.OpDelete(d.GetLastWebhookRequestPath(id), clientv3.WithPrefix()),
355-
clientv3.OpDelete(WebhookAuthPrefix+id, clientv3.WithPrefix()),
356-
)
321+
322+
tetcd.DeleteTx(txn, InternalConfig.Webhooks.Active().Key(id), clientv3.WithPrefix())
323+
tetcd.DeleteTx(txn, InternalConfig.Webhooks.Auth().Key(id), clientv3.WithPrefix())
324+
// this is a little gross, it might be better to make last requests a map
325+
tetcd.DeleteTx(txn, InternalConfig.Webhooks.LastRequests.Data().Key(id), clientv3.WithPrefix())
326+
tetcd.DeleteTx(txn, InternalConfig.Webhooks.LastRequests.Status().Key(id), clientv3.WithPrefix())
327+
tetcd.DeleteTx(txn, InternalConfig.Webhooks.LastRequests.Time().Key(id), clientv3.WithPrefix())
357328
}
358329

359330
_, err := d.etcd.Txn(context.Background()).Then(ops...).Commit()

internal/interfaces/db.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ type Webhooks interface {
5656
WebhookRecordLastRequest(id, authHeader, request string) error
5757

5858
CreateWebhook(webhook data.WebhookCreateRequestDTO) error
59-
GetWebhook(id string) (data.WebhookGetResponseDTO, error)
6059
GetWebhooks() (hooks []data.WebhookGetResponseDTO, err error)
6160
DeleteWebhooks(ids []string) error
6261

0 commit comments

Comments
 (0)