Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions doc/20-HTTP-API.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@ After creating a source with type _Other_ in Icinga Notifications Web,
the specified credentials can be used for HTTP Basic Authentication of a JSON-encoded
[Event](https://github.com/Icinga/icinga-notifications/blob/main/internal/event/event.go).

The authentication is performed via HTTP Basic Authentication, expecting `source-${id}` as the username,
`${id}` being the source's `id` within the database, and the configured password.
The authentication is performed via HTTP Basic Authentication using the source's username and password.

!!! tip

Before Icinga Notification version 0.2.0, the username was a fixed string based on the source ID, such as `source-${id}`.
These names were migrated for release version 0.2.0, but can now be altered within Icinga Notifications Web.

```
curl -v -u 'source-2:insecureinsecure' -d '@-' 'http://localhost:5680/process-event' <<EOF
Expand Down
29 changes: 12 additions & 17 deletions internal/config/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package config

import (
"context"
"crypto/subtle"
"database/sql"
"errors"
"fmt"
Expand All @@ -15,7 +16,6 @@ import (
"github.com/icinga/icinga-notifications/internal/timeperiod"
"go.uber.org/zap"
"golang.org/x/crypto/bcrypt"
"strconv"
"strings"
"sync"
"time"
Expand Down Expand Up @@ -213,29 +213,24 @@ func (r *RuntimeConfig) GetSourceFromCredentials(user, pass string, logger *logg
r.RLock()
defer r.RUnlock()

sourceIdRaw, sourceIdOk := strings.CutPrefix(user, "source-")
if !sourceIdOk {
logger.Debugw("Cannot extract source ID from HTTP basic auth username", zap.String("user_input", user))
return nil
}
sourceId, err := strconv.ParseInt(sourceIdRaw, 10, 64)
if err != nil {
logger.Debugw("Cannot convert extracted source Id to int", zap.String("user_input", user), zap.Error(err))
return nil
var src *Source
for _, tmpSrc := range r.Sources {
if subtle.ConstantTimeCompare([]byte(tmpSrc.ListenerUsername), []byte(user)) == 1 {
src = tmpSrc
break
}
}

src, ok := r.Sources[sourceId]
if !ok {
logger.Debugw("Cannot check credentials for unknown source ID", zap.Int64("id", sourceId))
if src == nil {
logger.Debugw("Cannot find source for username", zap.String("user", user))
return nil
}

err = src.PasswordCompare([]byte(pass))
err := src.PasswordCompare([]byte(pass))
if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
logger.Debugw("Invalid password for this source", zap.Int64("id", sourceId))
logger.Debugw("Invalid password for source", zap.Int64("id", src.ID))
return nil
} else if err != nil {
logger.Errorw("Failed to verify password for this source", zap.Int64("id", sourceId), zap.Error(err))
logger.Errorw("Failed to verify password for source", zap.Int64("id", src.ID), zap.Error(err))
return nil
}

Expand Down
5 changes: 3 additions & 2 deletions internal/config/source.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ type Source struct {
Type string `db:"type"`
Name string `db:"name"`

ListenerPasswordHash string `db:"listener_password_hash"`
listenerPassword []byte `db:"-"`
ListenerUsername string `db:"listener_username"`
ListenerPasswordHash string `db:"listener_password_hash" json:"-"`
listenerPassword []byte `db:"-" json:"-"`
listenerPasswordMutex sync.Mutex
}

Expand Down
3 changes: 2 additions & 1 deletion internal/object/objects_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,14 @@ func TestRestoreMutedObjects(t *testing.T) {
"type": "notifications",
"name": "Icinga Notifications",
"changed_at": int64(1720702049000),
"user": "jane.doe",
"pwd_hash": "$2y$", // Needed to pass the database constraint.
}
// We can't use config.Source here unfortunately due to cyclic import error!
id, err := database.InsertObtainID(
ctx,
tx,
`INSERT INTO source (type, name, changed_at, listener_password_hash) VALUES (:type, :name, :changed_at, :pwd_hash)`,
`INSERT INTO source (type, name, changed_at, listener_username, listener_password_hash) VALUES (:type, :name, :changed_at, :user, :pwd_hash)`,
args)
require.NoError(t, err, "populating source table should not fail")

Expand Down
7 changes: 4 additions & 3 deletions schema/mysql/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -206,14 +206,15 @@ CREATE TABLE source (
-- will likely need a distinguishing value for multiple sources of the same type in the future, like for example
-- the Icinga DB environment ID for Icinga 2 sources

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

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

CONSTRAINT uk_source_listener_username UNIQUE(listener_username),

-- The hash is a PHP password_hash with PASSWORD_DEFAULT algorithm, defaulting to bcrypt. This check roughly ensures
-- that listener_password_hash can only be populated with bcrypt hashes.
-- https://icinga.com/docs/icinga-web/latest/doc/20-Advanced-Topics/#manual-user-creation-for-database-authentication-backend
Expand Down
6 changes: 6 additions & 0 deletions schema/mysql/upgrades/001.sql
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,9 @@ ALTER TABLE rule

UPDATE rule SET source_id = (SELECT id FROM source WHERE type = 'icinga2');
ALTER TABLE rule MODIFY COLUMN source_id bigint NOT NULL;

ALTER TABLE source ADD COLUMN listener_username varchar(255);
UPDATE source SET listener_username = CONCAT('source-', source.id);
ALTER TABLE source
ALTER COLUMN listener_username SET NOT NULL,
ADD CONSTRAINT uk_source_listener_username UNIQUE(listener_username);
7 changes: 4 additions & 3 deletions schema/pgsql/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -238,14 +238,15 @@ CREATE TABLE source (
-- will likely need a distinguishing value for multiple sources of the same type in the future, like for example
-- the Icinga DB environment ID for Icinga 2 sources

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

changed_at bigint NOT NULL,
deleted boolenum NOT NULL DEFAULT 'n',

CONSTRAINT uk_source_listener_username UNIQUE(listener_username),

-- The hash is a PHP password_hash with PASSWORD_DEFAULT algorithm, defaulting to bcrypt. This check roughly ensures
-- that listener_password_hash can only be populated with bcrypt hashes.
-- https://icinga.com/docs/icinga-web/latest/doc/20-Advanced-Topics/#manual-user-creation-for-database-authentication-backend
Expand Down
6 changes: 6 additions & 0 deletions schema/pgsql/upgrades/001.sql
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,9 @@ ALTER TABLE rule

UPDATE rule SET source_id = (SELECT id FROM source WHERE type = 'icinga2');
ALTER TABLE rule ALTER COLUMN source_id SET NOT NULL;

ALTER TABLE source ADD COLUMN listener_username varchar(255);
UPDATE source SET listener_username = CONCAT('source-', source.id);
ALTER TABLE source
ALTER COLUMN listener_username SET NOT NULL,
ADD CONSTRAINT uk_source_listener_username UNIQUE(listener_username);
Loading