Skip to content

Commit 3ac0733

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 a022fdd commit 3ac0733

File tree

8 files changed

+38
-24
lines changed

8 files changed

+38
-24
lines changed

doc/20-HTTP-API.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@ After creating a source in Icinga Notifications Web,
1111
the specified credentials can be used via HTTP Basic Authentication to submit a JSON-encoded
1212
[`Event`](https://github.com/Icinga/icinga-go-library/blob/main/notifications/event/event.go).
1313

14-
The authentication is performed via HTTP Basic Authentication, expecting `source-${id}` as the username,
15-
`${id}` being the source's `id` within the database, and the configured password.
14+
The authentication is performed via HTTP Basic Authentication using the source's username and password.
15+
16+
!!! info
17+
18+
Before Icinga Notifications version 0.2.0, the username was a fixed string based on the source ID, such as `source-${id}`.
19+
When upgrading a setup from an earlier version, these usernames are still valid, but can be changed in Icinga Notifications Web.
1620

1721
Events sent to Icinga Notifications are expected to match rules that describe further event escalations.
1822
These rules can be created in the web interface.

internal/config/runtime.go

Lines changed: 15 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
"github.com/icinga/icinga-go-library/database"
@@ -13,7 +14,6 @@ import (
1314
"github.com/icinga/icinga-notifications/internal/timeperiod"
1415
"go.uber.org/zap"
1516
"golang.org/x/crypto/bcrypt"
16-
"strconv"
1717
"strings"
1818
"sync"
1919
"time"
@@ -205,29 +205,27 @@ func (r *RuntimeConfig) GetSourceFromCredentials(user, pass string, logger *logg
205205
r.RLock()
206206
defer r.RUnlock()
207207

208-
sourceIdRaw, sourceIdOk := strings.CutPrefix(user, "source-")
209-
if !sourceIdOk {
210-
logger.Debugw("Cannot extract source ID from HTTP basic auth username", zap.String("user_input", user))
211-
return nil
212-
}
213-
sourceId, err := strconv.ParseInt(sourceIdRaw, 10, 64)
214-
if err != nil {
215-
logger.Debugw("Cannot convert extracted source Id to int", zap.String("user_input", user), zap.Error(err))
216-
return nil
208+
var src *Source
209+
for _, tmpSrc := range r.Sources {
210+
if !tmpSrc.ListenerUsername.Valid {
211+
continue
212+
}
213+
if subtle.ConstantTimeCompare([]byte(tmpSrc.ListenerUsername.String), []byte(user)) == 1 {
214+
src = tmpSrc
215+
break
216+
}
217217
}
218-
219-
src, ok := r.Sources[sourceId]
220-
if !ok {
221-
logger.Debugw("Cannot check credentials for unknown source ID", zap.Int64("id", sourceId))
218+
if src == nil {
219+
logger.Debugw("Cannot find source for username", zap.String("user", user))
222220
return nil
223221
}
224222

225-
err = src.PasswordCompare([]byte(pass))
223+
err := src.PasswordCompare([]byte(pass))
226224
if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
227-
logger.Debugw("Invalid password for this source", zap.Int64("id", sourceId))
225+
logger.Debugw("Invalid password for source", zap.Int64("id", src.ID))
228226
return nil
229227
} else if err != nil {
230-
logger.Errorw("Failed to verify password for this source", zap.Int64("id", sourceId), zap.Error(err))
228+
logger.Errorw("Failed to verify password for source", zap.Int64("id", src.ID), zap.Error(err))
231229
return nil
232230
}
233231

internal/config/source.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type Source struct {
1717
Type string `db:"type"`
1818
Name string `db:"name"`
1919

20+
ListenerUsername types.String `db:"listener_username"`
2021
ListenerPasswordHash types.String `db:"listener_password_hash"`
2122
listenerPassword []byte `db:"-"`
2223
listenerPasswordMutex sync.Mutex

internal/object/objects_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,14 @@ func TestRestoreMutedObjects(t *testing.T) {
2424
"type": "notifications",
2525
"name": "Icinga Notifications",
2626
"changed_at": int64(1720702049000),
27+
"user": "jane.doe",
2728
"pwd_hash": "$2y$", // Needed to pass the database constraint.
2829
}
2930
// We can't use config.Source here unfortunately due to cyclic import error!
3031
id, err := database.InsertObtainID(
3132
ctx,
3233
tx,
33-
`INSERT INTO source (type, name, changed_at, listener_password_hash) VALUES (:type, :name, :changed_at, :pwd_hash)`,
34+
`INSERT INTO source (type, name, changed_at, listener_username, listener_password_hash) VALUES (:type, :name, :changed_at, :user, :pwd_hash)`,
3435
args)
3536
require.NoError(t, err, "populating source table should not fail")
3637

schema/mysql/schema.sql

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,13 +212,15 @@ CREATE TABLE source (
212212
-- will likely need a distinguishing value for multiple sources of the same type in the future, like for example
213213
-- the Icinga DB environment ID for Icinga 2 sources
214214

215-
-- This column is required to limit API access for incoming connections to the Listener.
216-
-- The username will be "source-${id}", allowing early verification.
215+
-- listener_{username,password_hash} are required to limit API access for incoming connections to the Listener.
216+
listener_username varchar(255),
217217
listener_password_hash text,
218218

219219
changed_at bigint NOT NULL,
220220
deleted enum('n', 'y') NOT NULL DEFAULT 'n',
221221

222+
CONSTRAINT uk_source_listener_username UNIQUE(listener_username),
223+
222224
-- The hash is a PHP password_hash with PASSWORD_DEFAULT algorithm, defaulting to bcrypt. This check roughly ensures
223225
-- that listener_password_hash can only be populated with bcrypt hashes.
224226
-- https://icinga.com/docs/icinga-web/latest/doc/20-Advanced-Topics/#manual-user-creation-for-database-authentication-backend
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
ALTER TABLE source ADD COLUMN listener_username varchar(255) AFTER name;
2+
UPDATE source SET listener_username = CONCAT('source-', source.id);
3+
ALTER TABLE source ADD CONSTRAINT uk_source_listener_username UNIQUE(listener_username);

schema/pgsql/schema.sql

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,13 +244,15 @@ CREATE TABLE source (
244244
-- will likely need a distinguishing value for multiple sources of the same type in the future, like for example
245245
-- the Icinga DB environment ID for Icinga 2 sources
246246

247-
-- This column is required to limit API access for incoming connections to the Listener.
248-
-- The username will be "source-${id}", allowing early verification.
247+
-- listener_{username,password_hash} are required to limit API access for incoming connections to the Listener.
248+
listener_username varchar(255),
249249
listener_password_hash text,
250250

251251
changed_at bigint NOT NULL,
252252
deleted boolenum NOT NULL DEFAULT 'n',
253253

254+
CONSTRAINT uk_source_listener_username UNIQUE(listener_username),
255+
254256
-- The hash is a PHP password_hash with PASSWORD_DEFAULT algorithm, defaulting to bcrypt. This check roughly ensures
255257
-- that listener_password_hash can only be populated with bcrypt hashes.
256258
-- https://icinga.com/docs/icinga-web/latest/doc/20-Advanced-Topics/#manual-user-creation-for-database-authentication-backend
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
ALTER TABLE source ADD COLUMN listener_username varchar(255);
2+
UPDATE source SET listener_username = CONCAT('source-', source.id);
3+
ALTER TABLE source ADD CONSTRAINT uk_source_listener_username UNIQUE(listener_username);

0 commit comments

Comments
 (0)