Skip to content

Commit 9e07bb4

Browse files
committed
Source: Introduce Usernames
Unique usernames were added to Sources for HTTP Authentication. Before, the HTTP Authentication expected a username based on the Source ID, such as "source-23". This was not very practical. Thus, an unique username column was introduced and the Listener's authentication code was adequately altered. Fixes #227.
1 parent 82ac037 commit 9e07bb4

File tree

7 files changed

+41
-27
lines changed

7 files changed

+41
-27
lines changed

doc/20-HTTP-API.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,12 @@ After creating a source with type _Other_ in Icinga Notifications Web,
1010
the specified credentials can be used for HTTP Basic Authentication of a JSON-encoded
1111
[Event](https://github.com/Icinga/icinga-notifications/blob/main/internal/event/event.go).
1212

13-
The authentication is performed via HTTP Basic Authentication, expecting `source-${id}` as the username,
14-
`${id}` being the source's `id` within the database, and the configured password.
13+
The authentication is performed via HTTP Basic Authentication using the source's username and password.
14+
15+
!!! tip
16+
17+
Before Icinga Notification version 0.2.0, the username was a fixed string based on the source ID, such as `source-${id}`.
18+
These names were migrated for release version 0.2.0, but can now be altered within Icinga Notifications Web.
1519

1620
```
1721
curl -v -u 'source-2:insecureinsecure' -d '@-' 'http://localhost:5680/process-event' <<EOF

internal/config/runtime.go

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

33
import (
44
"context"
5+
"crypto/subtle"
56
"database/sql"
67
"errors"
78
"fmt"
@@ -15,7 +16,6 @@ import (
1516
"github.com/icinga/icinga-notifications/internal/timeperiod"
1617
"go.uber.org/zap"
1718
"golang.org/x/crypto/bcrypt"
18-
"strconv"
1919
"strings"
2020
"sync"
2121
"time"
@@ -213,29 +213,24 @@ func (r *RuntimeConfig) GetSourceFromCredentials(user, pass string, logger *logg
213213
r.RLock()
214214
defer r.RUnlock()
215215

216-
sourceIdRaw, sourceIdOk := strings.CutPrefix(user, "source-")
217-
if !sourceIdOk {
218-
logger.Debugw("Cannot extract source ID from HTTP basic auth username", zap.String("user_input", user))
219-
return nil
220-
}
221-
sourceId, err := strconv.ParseInt(sourceIdRaw, 10, 64)
222-
if err != nil {
223-
logger.Debugw("Cannot convert extracted source Id to int", zap.String("user_input", user), zap.Error(err))
224-
return nil
216+
var src *Source
217+
for _, tmpSrc := range r.Sources {
218+
if subtle.ConstantTimeCompare([]byte(tmpSrc.ListenerUsername), []byte(user)) == 1 {
219+
src = tmpSrc
220+
break
221+
}
225222
}
226-
227-
src, ok := r.Sources[sourceId]
228-
if !ok {
229-
logger.Debugw("Cannot check credentials for unknown source ID", zap.Int64("id", sourceId))
223+
if src == nil {
224+
logger.Debugw("Cannot find source for username", zap.String("user", user))
230225
return nil
231226
}
232227

233-
err = src.PasswordCompare([]byte(pass))
228+
err := src.PasswordCompare([]byte(pass))
234229
if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
235-
logger.Debugw("Invalid password for this source", zap.Int64("id", sourceId))
230+
logger.Debugw("Invalid password for source", zap.Int64("id", src.ID))
236231
return nil
237232
} else if err != nil {
238-
logger.Errorw("Failed to verify password for this source", zap.Int64("id", sourceId), zap.Error(err))
233+
logger.Errorw("Failed to verify password for source", zap.Int64("id", src.ID), zap.Error(err))
239234
return nil
240235
}
241236

internal/config/source.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ type Source struct {
1616
Type string `db:"type"`
1717
Name string `db:"name"`
1818

19-
ListenerPasswordHash string `db:"listener_password_hash"`
20-
listenerPassword []byte `db:"-"`
19+
ListenerUsername string `db:"listener_username"`
20+
ListenerPasswordHash string `db:"listener_password_hash" json:"-"`
21+
listenerPassword []byte `db:"-" json:"-"`
2122
listenerPasswordMutex sync.Mutex
2223
}
2324

schema/mysql/schema.sql

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -206,14 +206,15 @@ CREATE TABLE source (
206206
-- will likely need a distinguishing value for multiple sources of the same type in the future, like for example
207207
-- the Icinga DB environment ID for Icinga 2 sources
208208

209-
-- The column listener_password_hash is type-dependent.
210-
-- This column is required to limit API access for incoming connections to the Listener.
211-
-- The username will be "source-${id}", allowing early verification.
209+
-- listener_{username,password_hash} are required to limit API access for incoming connections to the Listener.
210+
listener_username text NOT NULL,
212211
listener_password_hash text NOT NULL,
213212

214213
changed_at bigint NOT NULL,
215214
deleted enum('n', 'y') NOT NULL DEFAULT 'n',
216215

216+
CONSTRAINT uk_source_listener_username UNIQUE(listener_username),
217+
217218
-- The hash is a PHP password_hash with PASSWORD_DEFAULT algorithm, defaulting to bcrypt. This check roughly ensures
218219
-- that listener_password_hash can only be populated with bcrypt hashes.
219220
-- https://icinga.com/docs/icinga-web/latest/doc/20-Advanced-Topics/#manual-user-creation-for-database-authentication-backend

schema/mysql/upgrades/001.sql

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,9 @@ ALTER TABLE rule
1818

1919
UPDATE rule SET source_id = (SELECT id FROM source WHERE type = 'icinga2');
2020
ALTER TABLE rule MODIFY COLUMN source_id bigint NOT NULL;
21+
22+
ALTER TABLE source ADD COLUMN listener_username text;
23+
UPDATE source SET listener_username = CONCAT('source-', source.id);
24+
ALTER TABLE source
25+
ALTER COLUMN listener_username SET NOT NULL,
26+
ADD CONSTRAINT uk_source_listener_username UNIQUE(listener_username);

schema/pgsql/schema.sql

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -238,14 +238,15 @@ CREATE TABLE source (
238238
-- will likely need a distinguishing value for multiple sources of the same type in the future, like for example
239239
-- the Icinga DB environment ID for Icinga 2 sources
240240

241-
-- The column listener_password_hash is type-dependent.
242-
-- This column is required to limit API access for incoming connections to the Listener.
243-
-- The username will be "source-${id}", allowing early verification.
241+
-- listener_{username,password_hash} are required to limit API access for incoming connections to the Listener.
242+
listener_username text NOT NULL,
244243
listener_password_hash text NOT NULL,
245244

246245
changed_at bigint NOT NULL,
247246
deleted boolenum NOT NULL DEFAULT 'n',
248247

248+
CONSTRAINT uk_source_listener_username UNIQUE(listener_username),
249+
249250
-- The hash is a PHP password_hash with PASSWORD_DEFAULT algorithm, defaulting to bcrypt. This check roughly ensures
250251
-- that listener_password_hash can only be populated with bcrypt hashes.
251252
-- https://icinga.com/docs/icinga-web/latest/doc/20-Advanced-Topics/#manual-user-creation-for-database-authentication-backend

schema/pgsql/upgrades/001.sql

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,9 @@ ALTER TABLE rule
1818

1919
UPDATE rule SET source_id = (SELECT id FROM source WHERE type = 'icinga2');
2020
ALTER TABLE rule ALTER COLUMN source_id SET NOT NULL;
21+
22+
ALTER TABLE source ADD COLUMN listener_username text;
23+
UPDATE source SET listener_username = CONCAT('source-', source.id);
24+
ALTER TABLE source
25+
ALTER COLUMN listener_username SET NOT NULL,
26+
ADD CONSTRAINT uk_source_listener_username UNIQUE(listener_username);

0 commit comments

Comments
 (0)