@@ -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
4633type WebhookCreateRequestDTO struct {
47- Webhook
34+ config. Webhook
4835 AuthHeader string `json:"auth_header,omitempty" validate:"required,min=32,max=32"`
4936}
5037
5138type 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-
7153func (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
163141func (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
291258func (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
314280func (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 ()
0 commit comments