diff --git a/.mockery.yaml b/.mockery.yaml
new file mode 100644
index 0000000..d99e41f
--- /dev/null
+++ b/.mockery.yaml
@@ -0,0 +1,10 @@
+dir: "{{.InterfaceDir}}/mocks"
+pkgname: "mocks"
+structname: "{{.InterfaceName}}"
+filename: "{{.InterfaceName}}.go"
+all: true
+
+packages:
+ github.com/greenbone/opensight-golang-libraries/pkg/notifications:
+ config:
+ recursive: true
diff --git a/Makefile b/Makefile
index 1c22287..d9e334d 100644
--- a/Makefile
+++ b/Makefile
@@ -21,7 +21,7 @@ GO-MOD-UPGRADE = go run github.com/oligot/go-mod-upgrade@latest
SWAG = github.com/swaggo/swag/cmd/swag@v1.16.2
INSTALL_GOMARKDOC = go install github.com/princjef/gomarkdoc/cmd/gomarkdoc@latest
-INSTALL_MOCKERY = go install github.com/vektra/mockery/v2@v2.44.1
+MOCKERY = github.com/vektra/mockery/v3@v3.5.1
OS="$(shell go env var GOOS | xargs)"
@@ -49,9 +49,11 @@ format: ## format and tidy
go fmt ./...
generate-code: ## create mocks and enums
- @ echo "\033[36m Generate mocks and enums \033[0m"
+ @ echo "\033[36m Generate enums \033[0m"
go get github.com/abice/go-enum
go generate ./...
+ @ echo "\033[36m Generate mocks \033[0m"
+ go run $(MOCKERY) --log-level warn
.PHONY: lint
@@ -119,7 +121,7 @@ test-postgres:
.PHONY: generate_docs
generate_docs: check_tools
gomarkdoc -e --output '{{.Dir}}/README.md' \
- --exclude-dirs .,./pkg/configReader/helper,./pkg/dbcrypt/config,./pkg/openSearch/openSearchClient/config \
+ --exclude-dirs .,./pkg/configReader/helper,./pkg/dbcrypt/config,./pkg/openSearch/openSearchClient/config,./pkg/notifications/mocks \
./pkg/...
check_tools:
diff --git a/pkg/auth/README.md b/pkg/auth/README.md
index dd5b420..531c3c5 100644
--- a/pkg/auth/README.md
+++ b/pkg/auth/README.md
@@ -12,14 +12,52 @@ Package auth provides a client to authenticate against a Keycloak server.
## Index
+- [type ClientCredentials](<#ClientCredentials>)
+- [type Clock](<#Clock>)
+- [type Credentials](<#Credentials>)
- [type KeycloakClient](<#KeycloakClient>)
- - [func NewKeycloakClient\(httpClient \*http.Client, cfg KeycloakConfig\) \*KeycloakClient](<#NewKeycloakClient>)
+ - [func NewKeycloakClient\(httpClient \*http.Client, cfg KeycloakConfig, credentials Credentials\) \*KeycloakClient](<#NewKeycloakClient>)
- [func \(c \*KeycloakClient\) GetToken\(ctx context.Context\) \(string, error\)](<#KeycloakClient.GetToken>)
- [type KeycloakConfig](<#KeycloakConfig>)
+- [type ResourceOwnerCredentials](<#ResourceOwnerCredentials>)
+
+## type [ClientCredentials]()
+
+ClientCredentials to authenticate via \`Client credentials grant\` flow. Ref: https://www.keycloak.org/docs/latest/server_admin/index.html#_client_credentials_grant
+
+```go
+type ClientCredentials struct {
+ ClientID string
+ ClientSecret string
+}
+```
+
+
+## type [Clock]()
+
+
+
+```go
+type Clock interface {
+ Now() time.Time
+}
+```
+
+
+## type [Credentials]()
+
+Credentials holds the required credentials and determines the used auth type.
+
+```go
+type Credentials interface {
+ // contains filtered or unexported methods
+}
+```
+
-## type [KeycloakClient]()
+## type [KeycloakClient]()
KeycloakClient can be used to authenticate against a Keycloak server.
@@ -30,38 +68,48 @@ type KeycloakClient struct {
```
-### func [NewKeycloakClient]()
+### func [NewKeycloakClient]()
```go
-func NewKeycloakClient(httpClient *http.Client, cfg KeycloakConfig) *KeycloakClient
+func NewKeycloakClient(httpClient *http.Client, cfg KeycloakConfig, credentials Credentials) *KeycloakClient
```
-NewKeycloakClient creates a new KeycloakClient.
+NewKeycloakClient creates a new KeycloakClient. Passed [Credentials](<#Credentials>) determines the used auth type.
-### func \(\*KeycloakClient\) [GetToken]()
+### func \(\*KeycloakClient\) [GetToken]()
```go
func (c *KeycloakClient) GetToken(ctx context.Context) (string, error)
```
-GetToken retrieves a valid access token. The token is cached and refreshed before expiry. The token is obtained by \`Resource owner password credentials grant\` flow. Ref: https://www.keycloak.org/docs/latest/server_admin/index.html#_oidc-auth-flows-direct
+GetToken retrieves a valid access token. The token is cached and refreshed before expiry.
-## type [KeycloakConfig]()
+## type [KeycloakConfig]()
KeycloakConfig holds the credentials and configuration details
```go
type KeycloakConfig struct {
- ClientID string
- Username string
- Password string
AuthURL string
KeycloakRealm string
}
```
+
+## type [ResourceOwnerCredentials]()
+
+ResourceOwnerCredentials to authenticate via \`Resource owner password credentials grant\` flow. Ref: https://www.keycloak.org/docs/latest/server_admin/index.html#_oidc-auth-flows-direct
+
+```go
+type ResourceOwnerCredentials struct {
+ ClientID string
+ Username string
+ Password string
+}
+```
+
Generated by [gomarkdoc]()
diff --git a/pkg/auth/auth_client.go b/pkg/auth/auth_client.go
index c1c26d0..addefc8 100644
--- a/pkg/auth/auth_client.go
+++ b/pkg/auth/auth_client.go
@@ -29,11 +29,8 @@ const tokenRefreshMargin = 10 * time.Second
// KeycloakConfig holds the credentials and configuration details
type KeycloakConfig struct {
- ClientID string
- Username string
- Password string //nolint:gosec
- AuthURL string
- KeycloakRealm string
+ AuthURL string
+ Realm string
}
type tokenInfo struct {
@@ -46,28 +43,67 @@ type authResponse struct {
ExpiresIn int `json:"expires_in"` // in seconds
}
+// Credentials holds the required credentials and determines the used auth type.
+type Credentials interface {
+ constructUrlValues() url.Values // unexported method to prevent external implementation
+}
+
+// ClientCredentials to authenticate via `Client credentials grant` flow.
+// Ref: https://www.keycloak.org/docs/latest/server_admin/index.html#_client_credentials_grant
+type ClientCredentials struct {
+ ClientID string
+ ClientSecret string
+}
+
+func (c ClientCredentials) constructUrlValues() url.Values {
+ urlValues := url.Values{}
+ urlValues.Set("grant_type", "client_credentials")
+ urlValues.Set("client_id", c.ClientID)
+ urlValues.Set("client_secret", c.ClientSecret)
+
+ return urlValues
+}
+
+// ResourceOwnerCredentials to authenticate via `Resource owner password credentials grant` flow.
+// Ref: https://www.keycloak.org/docs/latest/server_admin/index.html#_oidc-auth-flows-direct
+type ResourceOwnerCredentials struct {
+ ClientID string
+ Username string
+ Password string
+}
+
+func (c ResourceOwnerCredentials) constructUrlValues() url.Values {
+ urlValues := url.Values{}
+ urlValues.Set("grant_type", "password")
+ urlValues.Set("client_id", c.ClientID)
+ urlValues.Set("username", c.Username)
+ urlValues.Set("password", c.Password)
+
+ return urlValues
+}
+
// KeycloakClient can be used to authenticate against a Keycloak server.
type KeycloakClient struct {
- httpClient *http.Client
- cfg KeycloakConfig
- tokenInfo tokenInfo
- tokenMutex sync.RWMutex
+ httpClient *http.Client
+ cfg KeycloakConfig
+ credentials Credentials
+ tokenInfo tokenInfo
+ tokenMutex sync.RWMutex
clock Clock // to mock time in tests
}
-// NewKeycloakClient creates a new KeycloakClient.
-func NewKeycloakClient(httpClient *http.Client, cfg KeycloakConfig) *KeycloakClient {
+// NewKeycloakClient creates a new KeycloakClient. Passed [Credentials] determines the used auth type.
+func NewKeycloakClient(httpClient *http.Client, cfg KeycloakConfig, credentials Credentials) *KeycloakClient {
return &KeycloakClient{
- httpClient: httpClient,
- cfg: cfg,
- clock: realClock{},
+ httpClient: httpClient,
+ cfg: cfg,
+ credentials: credentials,
+ clock: realClock{},
}
}
// GetToken retrieves a valid access token. The token is cached and refreshed before expiry.
-// The token is obtained by `Resource owner password credentials grant` flow.
-// Ref: https://www.keycloak.org/docs/latest/server_admin/index.html#_oidc-auth-flows-direct
func (c *KeycloakClient) GetToken(ctx context.Context) (string, error) {
token, ok := c.getCachedToken()
if ok {
@@ -106,14 +142,10 @@ func (c *KeycloakClient) getCachedToken() (token string, ok bool) {
}
func (c *KeycloakClient) requestToken(ctx context.Context) (*authResponse, error) {
- data := url.Values{}
- data.Set("client_id", c.cfg.ClientID)
- data.Set("password", c.cfg.Password)
- data.Set("username", c.cfg.Username)
- data.Set("grant_type", "password")
+ data := c.credentials.constructUrlValues()
authenticationURL := fmt.Sprintf("%s/realms/%s/protocol/openid-connect/token",
- c.cfg.AuthURL, c.cfg.KeycloakRealm)
+ c.cfg.AuthURL, c.cfg.Realm)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, authenticationURL, strings.NewReader(data.Encode()))
if err != nil {
diff --git a/pkg/auth/auth_client_test.go b/pkg/auth/auth_client_test.go
index 2eb1d22..59445e9 100644
--- a/pkg/auth/auth_client_test.go
+++ b/pkg/auth/auth_client_test.go
@@ -9,6 +9,7 @@ import (
"fmt"
"net/http"
"net/http/httptest"
+ "net/url"
"sync/atomic"
"testing"
"time"
@@ -19,21 +20,50 @@ import (
func TestKeycloakClient_GetToken(t *testing.T) {
tests := map[string]struct {
- responseBody string
- responseCode int
- wantErr bool
- wantToken string
+ credentials Credentials
+ responseBody string
+ responseCode int
+ wantUrlValues url.Values
+ wantErr bool
+ wantToken string
}{
- "successful token retrieval": {
+ "successful token retrieval (client credentials)": {
+ credentials: ClientCredentials{ClientID: "test-client-id", ClientSecret: "test-client-secret"},
+
+ responseBody: `{"access_token": "test-token", "expires_in": 3600}`,
+ responseCode: http.StatusOK,
+ wantUrlValues: url.Values{
+ "grant_type": {"client_credentials"},
+ "client_id": {"test-client-id"},
+ "client_secret": {"test-client-secret"},
+ },
+ wantErr: false,
+ wantToken: "test-token",
+ },
+ "successful token retrieval (resource owner credentials)": {
+ credentials: ResourceOwnerCredentials{ClientID: "test-client-id", Username: "test-user", Password: "test-password"},
+
responseBody: `{"access_token": "test-token", "expires_in": 3600}`,
responseCode: http.StatusOK,
- wantErr: false,
- wantToken: "test-token",
+ wantUrlValues: url.Values{
+ "grant_type": {"password"},
+ "client_id": {"test-client-id"},
+ "username": {"test-user"},
+ "password": {"test-password"},
+ },
+ wantErr: false,
+ wantToken: "test-token",
},
"failed authentication": {
+ credentials: ClientCredentials{ClientID: "invalid-client-id", ClientSecret: "invalid-client-secret"},
responseBody: `{}`,
responseCode: http.StatusUnauthorized,
- wantErr: true,
+ wantUrlValues: url.Values{
+ "grant_type": {"client_credentials"},
+ "client_id": {"invalid-client-id"},
+ "client_secret": {"invalid-client-secret"},
+ },
+ wantErr: true,
},
}
for name, tt := range tests {
@@ -43,8 +73,14 @@ func TestKeycloakClient_GetToken(t *testing.T) {
w http.ResponseWriter, r *http.Request,
) {
serverCallCount.Add(1)
+
+ // Verify required URL parameters are present
+ err := r.ParseForm()
+ require.NoError(t, err)
+ require.Equal(t, tt.wantUrlValues, r.Form)
+
w.WriteHeader(tt.responseCode)
- _, err := w.Write([]byte(tt.responseBody))
+ _, err = w.Write([]byte(tt.responseBody))
require.NoError(t, err)
}))
defer mockServer.Close()
@@ -52,7 +88,7 @@ func TestKeycloakClient_GetToken(t *testing.T) {
client := NewKeycloakClient(http.DefaultClient, KeycloakConfig{
AuthURL: mockServer.URL,
// the other fields are also required in real scenario, but omit here for brevity
- })
+ }, tt.credentials)
gotToken, err := client.GetToken(context.Background())
assert.Greater(t, serverCallCount.Load(), int32(0), "server was not called")
@@ -123,7 +159,7 @@ func TestKeycloakClient_GetToken_Refresh(t *testing.T) {
client := NewKeycloakClient(http.DefaultClient, KeycloakConfig{
AuthURL: mockServer.URL,
// the other fields are also required in real scenario, but omit here for brevity
- })
+ }, ClientCredentials{})
client.clock = fakeClock
_, err := client.GetToken(context.Background())
diff --git a/pkg/notifications/README.md b/pkg/notifications/README.md
index 46c8e42..c38ec9b 100644
--- a/pkg/notifications/README.md
+++ b/pkg/notifications/README.md
@@ -12,16 +12,44 @@ Package Notifications provides a client to communicate with the OpenSight Notifi
## Index
+- [Variables](<#variables>)
+- [type AuthClient](<#AuthClient>)
- [type Client](<#Client>)
- - [func NewClient\(httpClient \*http.Client, config Config, authCfg auth.KeycloakConfig\) \*Client](<#NewClient>)
+ - [func NewClient\(httpClient \*http.Client, config Config, authClient AuthClient\) \*Client](<#NewClient>)
- [func \(c \*Client\) CreateNotification\(ctx context.Context, notification Notification\) error](<#Client.CreateNotification>)
+ - [func \(c \*Client\) RegisterOrigins\(ctx context.Context, serviceID string, origins \[\]Origin\) error](<#Client.RegisterOrigins>)
- [type Config](<#Config>)
- [type Level](<#Level>)
- [type Notification](<#Notification>)
+- [type Origin](<#Origin>)
+## Variables
+
+
+
+```go
+var AllowedLevels = []Level{
+ LevelInfo,
+ LevelWarning,
+ LevelError,
+ LevelUrgent,
+}
+```
+
+
+## type [AuthClient]()
+
+
+
+```go
+type AuthClient interface {
+ GetToken(ctx context.Context) (string, error)
+}
+```
+
-## type [Client]()
+## type [Client]()
Client can be used to send notifications
@@ -32,16 +60,16 @@ type Client struct {
```
-### func [NewClient]()
+### func [NewClient]()
```go
-func NewClient(httpClient *http.Client, config Config, authCfg auth.KeycloakConfig) *Client
+func NewClient(httpClient *http.Client, config Config, authClient AuthClient) *Client
```
NewClient returns a new [Client](<#Client>) with the notification service address \(host:port\) set. As httpClient you can use e.g. \[http.DefaultClient\].
-### func \(\*Client\) [CreateNotification]()
+### func \(\*Client\) [CreateNotification]()
```go
func (c *Client) CreateNotification(ctx context.Context, notification Notification) error
@@ -49,8 +77,17 @@ func (c *Client) CreateNotification(ctx context.Context, notification Notificati
CreateNotification sends a notification to the notification service. It is retried up to the configured number of retries with an exponential backoff, So it can take some time until the functions returns.
+
+### func \(\*Client\) [RegisterOrigins]()
+
+```go
+func (c *Client) RegisterOrigins(ctx context.Context, serviceID string, origins []Origin) error
+```
+
+
+
-## type [Config]()
+## type [Config]()
Config configures the notification service client
@@ -79,6 +116,7 @@ const (
LevelInfo Level = "info"
LevelWarning Level = "warning"
LevelError Level = "error"
+ LevelUrgent Level = "urgent"
)
```
@@ -101,6 +139,18 @@ type Notification struct {
}
```
+
+## type [Origin]()
+
+
+
+```go
+type Origin struct {
+ Name string `json:"name"`
+ Class string `json:"class"`
+}
+```
+
Generated by [gomarkdoc]()
diff --git a/pkg/notifications/mocks/AuthClient.go b/pkg/notifications/mocks/AuthClient.go
new file mode 100644
index 0000000..6d0618d
--- /dev/null
+++ b/pkg/notifications/mocks/AuthClient.go
@@ -0,0 +1,98 @@
+// Code generated by mockery; DO NOT EDIT.
+// github.com/vektra/mockery
+// template: testify
+
+package mocks
+
+import (
+ "context"
+
+ mock "github.com/stretchr/testify/mock"
+)
+
+// NewAuthClient creates a new instance of AuthClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+// The first argument is typically a *testing.T value.
+func NewAuthClient(t interface {
+ mock.TestingT
+ Cleanup(func())
+}) *AuthClient {
+ mock := &AuthClient{}
+ mock.Mock.Test(t)
+
+ t.Cleanup(func() { mock.AssertExpectations(t) })
+
+ return mock
+}
+
+// AuthClient is an autogenerated mock type for the AuthClient type
+type AuthClient struct {
+ mock.Mock
+}
+
+type AuthClient_Expecter struct {
+ mock *mock.Mock
+}
+
+func (_m *AuthClient) EXPECT() *AuthClient_Expecter {
+ return &AuthClient_Expecter{mock: &_m.Mock}
+}
+
+// GetToken provides a mock function for the type AuthClient
+func (_mock *AuthClient) GetToken(ctx context.Context) (string, error) {
+ ret := _mock.Called(ctx)
+
+ if len(ret) == 0 {
+ panic("no return value specified for GetToken")
+ }
+
+ var r0 string
+ var r1 error
+ if returnFunc, ok := ret.Get(0).(func(context.Context) (string, error)); ok {
+ return returnFunc(ctx)
+ }
+ if returnFunc, ok := ret.Get(0).(func(context.Context) string); ok {
+ r0 = returnFunc(ctx)
+ } else {
+ r0 = ret.Get(0).(string)
+ }
+ if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok {
+ r1 = returnFunc(ctx)
+ } else {
+ r1 = ret.Error(1)
+ }
+ return r0, r1
+}
+
+// AuthClient_GetToken_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetToken'
+type AuthClient_GetToken_Call struct {
+ *mock.Call
+}
+
+// GetToken is a helper method to define mock.On call
+// - ctx context.Context
+func (_e *AuthClient_Expecter) GetToken(ctx interface{}) *AuthClient_GetToken_Call {
+ return &AuthClient_GetToken_Call{Call: _e.mock.On("GetToken", ctx)}
+}
+
+func (_c *AuthClient_GetToken_Call) Run(run func(ctx context.Context)) *AuthClient_GetToken_Call {
+ _c.Call.Run(func(args mock.Arguments) {
+ var arg0 context.Context
+ if args[0] != nil {
+ arg0 = args[0].(context.Context)
+ }
+ run(
+ arg0,
+ )
+ })
+ return _c
+}
+
+func (_c *AuthClient_GetToken_Call) Return(s string, err error) *AuthClient_GetToken_Call {
+ _c.Call.Return(s, err)
+ return _c
+}
+
+func (_c *AuthClient_GetToken_Call) RunAndReturn(run func(ctx context.Context) (string, error)) *AuthClient_GetToken_Call {
+ _c.Call.Return(run)
+ return _c
+}
diff --git a/pkg/notifications/notification.go b/pkg/notifications/notification.go
index 1491a50..6ad48b7 100644
--- a/pkg/notifications/notification.go
+++ b/pkg/notifications/notification.go
@@ -15,7 +15,6 @@ import (
"net/url"
"time"
- "github.com/greenbone/opensight-golang-libraries/pkg/auth"
"github.com/greenbone/opensight-golang-libraries/pkg/retryableRequest"
)
@@ -28,7 +27,7 @@ const (
// Client can be used to send notifications
type Client struct {
httpClient *http.Client
- authClient *auth.KeycloakClient
+ authClient AuthClient
notificationServiceAddress string
maxRetries int
retryWaitMin time.Duration
@@ -43,11 +42,13 @@ type Config struct {
RetryWaitMax time.Duration
}
+type AuthClient interface {
+ GetToken(ctx context.Context) (string, error)
+}
+
// NewClient returns a new [Client] with the notification service address (host:port) set.
// As httpClient you can use e.g. [http.DefaultClient].
-func NewClient(httpClient *http.Client, config Config, authCfg auth.KeycloakConfig) *Client {
- authClient := auth.NewKeycloakClient(httpClient, authCfg)
-
+func NewClient(httpClient *http.Client, config Config, authClient AuthClient) *Client {
return &Client{
httpClient: httpClient,
authClient: authClient,
diff --git a/pkg/notifications/notification_test.go b/pkg/notifications/notification_test.go
index dbe6937..cde3268 100644
--- a/pkg/notifications/notification_test.go
+++ b/pkg/notifications/notification_test.go
@@ -14,16 +14,17 @@ import (
"testing"
"time"
- "github.com/greenbone/opensight-golang-libraries/pkg/auth"
+ "github.com/greenbone/opensight-golang-libraries/pkg/notifications/mocks"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
type serverErrors struct { // set at most one of the fields to true
- fatalFail bool
- retryableFail bool
- authenticationFail bool
- unexpectedResponse bool
+ fatalFail bool
+ retryableFail bool
+ authClientFail bool
+ persistentInternalError bool
}
const checkForCurrentTimestamp = "marker to check for current timestamp"
@@ -94,14 +95,14 @@ func TestClient_CreateNotification(t *testing.T) {
{
name: "client fails on authentication error",
notification: notification,
- serverErrors: serverErrors{authenticationFail: true},
+ serverErrors: serverErrors{authClientFail: true},
wantNotificationSent: wantNotification,
wantErr: true,
},
{
name: "sending notification fails after maximum number of retries",
notification: notification,
- serverErrors: serverErrors{unexpectedResponse: true},
+ serverErrors: serverErrors{persistentInternalError: true},
wantNotificationSent: wantNotification,
wantErr: true,
},
@@ -110,9 +111,6 @@ func TestClient_CreateNotification(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
var serverCallCount atomic.Int32
- mockAuthServer := setupMockAuthServer(t, tt.serverErrors.authenticationFail)
- defer mockAuthServer.Close()
-
mockNotificationServer := setupMockNotificationServer(t, &serverCallCount,
tt.wantNotificationSent, tt.serverErrors)
defer mockNotificationServer.Close()
@@ -124,17 +122,17 @@ func TestClient_CreateNotification(t *testing.T) {
RetryWaitMax: time.Second,
}
- authentication := auth.KeycloakConfig{
- ClientID: "client_id",
- Username: "username",
- Password: "password",
- AuthURL: mockAuthServer.URL,
+ authClient := mocks.NewAuthClient(t)
+ if tt.serverErrors.authClientFail {
+ authClient.EXPECT().GetToken(mock.Anything).Return("", assert.AnError).Times(1)
+ } else {
+ authClient.EXPECT().GetToken(mock.Anything).Return("mock-token", nil).Times(1)
}
- client := NewClient(http.DefaultClient, config, authentication)
+ client := NewClient(http.DefaultClient, config, authClient)
err := client.CreateNotification(context.Background(), tt.notification)
- if !tt.serverErrors.authenticationFail {
+ if !tt.serverErrors.authClientFail {
assert.Greater(t, serverCallCount.Load(), int32(0), "notification server was not called")
}
@@ -147,18 +145,6 @@ func TestClient_CreateNotification(t *testing.T) {
}
}
-func setupMockAuthServer(t *testing.T, failAuth bool) *httptest.Server {
- return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- if failAuth {
- w.WriteHeader(http.StatusUnauthorized)
- return
- }
- w.Write([]byte(`{"access_token": "mock-token", "expires_in": 3600}`))
- err := json.NewEncoder(w).Encode(map[string]any{})
- require.NoError(t, err)
- }))
-}
-
func setupMockNotificationServer(t *testing.T, serverCallCount *atomic.Int32,
wantNotification notificationModel, errors serverErrors,
) *httptest.Server {
@@ -204,7 +190,7 @@ func setupMockNotificationServer(t *testing.T, serverCallCount *atomic.Int32,
return
}
}
- if errors.unexpectedResponse {
+ if errors.persistentInternalError {
w.WriteHeader(http.StatusInternalServerError)
return
}
diff --git a/pkg/swagger/README.md b/pkg/swagger/README.md
index 74c73de..61b3b2e 100644
--- a/pkg/swagger/README.md
+++ b/pkg/swagger/README.md
@@ -12,6 +12,7 @@ To use the latest swagger-ui (by using github.com/swaggo/files/v2) we needed to
## Sources used
To compile the new package we took the gin-echo package (https://github.com/swaggo/echo-swagger/blob/master/swagger.go) as a reference and added the gin based functionality.
+*Note:* It is not yet compatible with `Authorization Code + PKCE` login flow. If that becomes necessary see open PR of related package: https://github.com/swaggo/echo-swagger/pull/100 as a start.
## Usage
@@ -50,18 +51,25 @@ import "github.com/greenbone/opensight-golang-libraries/pkg/swagger"
## Index
-- [Variables](<#variables>)
-- [func DeepLinking\(deepLinking bool\) func\(\*Config\)](<#DeepLinking>)
-- [func DocExpansion\(docExpansion string\) func\(\*Config\)](<#DocExpansion>)
-- [func DomID\(domID string\) func\(\*Config\)](<#DomID>)
-- [func GinWrapHandler\(options ...func\(\*Config\)\) gin.HandlerFunc](<#GinWrapHandler>)
-- [func InstanceName\(instanceName string\) func\(\*Config\)](<#InstanceName>)
-- [func OAuth\(config \*OAuthConfig\) func\(\*Config\)](<#OAuth>)
-- [func PersistAuthorization\(persistAuthorization bool\) func\(\*Config\)](<#PersistAuthorization>)
-- [func SyntaxHighlight\(syntaxHighlight bool\) func\(\*Config\)](<#SyntaxHighlight>)
-- [func URL\(url string\) func\(\*Config\)](<#URL>)
-- [type Config](<#Config>)
-- [type OAuthConfig](<#OAuthConfig>)
+- [ginSwagger](#ginswagger)
+ - [Reason for implementation](#reason-for-implementation)
+ - [Sources used](#sources-used)
+ - [Usage](#usage)
+- [License](#license)
+- [ginSwagger](#ginswagger-1)
+ - [Index](#index)
+ - [Variables](#variables)
+ - [func DeepLinking](#func-deeplinking)
+ - [func DocExpansion](#func-docexpansion)
+ - [func DomID](#func-domid)
+ - [func GinWrapHandler](#func-ginwraphandler)
+ - [func InstanceName](#func-instancename)
+ - [func OAuth](#func-oauth)
+ - [func PersistAuthorization](#func-persistauthorization)
+ - [func SyntaxHighlight](#func-syntaxhighlight)
+ - [func URL](#func-url)
+ - [type Config](#type-config)
+ - [type OAuthConfig](#type-oauthconfig)
## Variables
diff --git a/pkg/swagger/ginSwagger.go b/pkg/swagger/ginSwagger.go
index 89d6171..6faf53a 100644
--- a/pkg/swagger/ginSwagger.go
+++ b/pkg/swagger/ginSwagger.go
@@ -9,6 +9,7 @@ import (
"net/http"
"path/filepath"
"regexp"
+ "strings"
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files/v2"
@@ -26,6 +27,7 @@ type Config struct {
PersistAuthorization bool
SyntaxHighlight bool
OAuth *OAuthConfig
+ CSPConnectSrc []string
}
type OAuthConfig struct {
@@ -82,6 +84,15 @@ func OAuth(config *OAuthConfig) func(*Config) {
}
}
+// CSPConnectSrc adds URLs to the Content Security Policy's connect-src directive.
+// This is necessary to allow the Swagger UI to communicate with Keycloak for authentication.
+// Note: The connect-src directive expects only scheme://host:port without paths
+func CSPConnectSrc(urls ...string) func(*Config) {
+ return func(c *Config) {
+ c.CSPConnectSrc = append(c.CSPConnectSrc, urls...)
+ }
+}
+
func newConfig(configFns ...func(*Config)) *Config {
config := Config{
URLs: []string{"doc.json", "doc.yaml"},
@@ -106,13 +117,23 @@ var WrapHandler = GinWrapHandler()
func GinWrapHandler(options ...func(*Config)) gin.HandlerFunc {
config := newConfig(options...)
index, _ := template.New("swagger_index.html").Parse(indexTemplate)
- re := regexp.MustCompile(`^(.*/)([^?].*)?[?|.]*$`)
+ re := regexp.MustCompile(`^(.*/)([^?]*)?(\?.*)?$`) // e.g. /api/users?sort=name => [ "api/", "users", "?sort=name" ]
return func(c *gin.Context) {
// Set security headers to protect against ClickJacking and XSS
c.Header("X-Frame-Options", "DENY")
c.Header("X-XSS-Protection", "1; mode=block")
- c.Header("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; frame-ancestors 'none'")
+ csp := []string{
+ "default-src 'self'",
+ "script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net",
+ "frame-ancestors 'none'",
+ }
+ if len(config.CSPConnectSrc) > 0 {
+ sources := append(config.CSPConnectSrc, "'self'")
+ connectSrc := "connect-src " + strings.Join(sources, " ")
+ csp = append(csp, connectSrc)
+ }
+ c.Header("Content-Security-Policy", strings.Join(csp, "; "))
c.Header("X-Content-Type-Options", "nosniff")
if c.Request.Method != http.MethodGet {