Skip to content
Merged
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
26 changes: 23 additions & 3 deletions pkg/auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ Package auth provides a client to authenticate against a Keycloak server.
- [type Credentials](<#Credentials>)
- [type KeycloakClient](<#KeycloakClient>)
- [func NewKeycloakClient\(httpClient \*http.Client, cfg KeycloakConfig, credentials Credentials\) \*KeycloakClient](<#NewKeycloakClient>)
- [func \(c \*KeycloakClient\) Config\(\) KeycloakConfig](<#KeycloakClient.Config>)
- [func \(c \*KeycloakClient\) GetToken\(ctx context.Context\) \(string, error\)](<#KeycloakClient.GetToken>)
- [func \(c \*KeycloakClient\) HTTPClient\(\) \*http.Client](<#KeycloakClient.HTTPClient>)
- [type KeycloakConfig](<#KeycloakConfig>)
- [type ResourceOwnerCredentials](<#ResourceOwnerCredentials>)

Expand Down Expand Up @@ -76,6 +78,15 @@ func NewKeycloakClient(httpClient *http.Client, cfg KeycloakConfig, credentials

NewKeycloakClient creates a new KeycloakClient. Passed [Credentials](<#Credentials>) determines the used auth type.

<a name="KeycloakClient.Config"></a>
### func \(\*KeycloakClient\) [Config](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/auth/auth_client.go#L184>)

```go
func (c *KeycloakClient) Config() KeycloakConfig
```

Config returns the KeycloakConfig used by the KeycloakClient.

<a name="KeycloakClient.GetToken"></a>
### func \(\*KeycloakClient\) [GetToken](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/auth/auth_client.go#L107>)

Expand All @@ -85,20 +96,29 @@ func (c *KeycloakClient) GetToken(ctx context.Context) (string, error)

GetToken retrieves a valid access token. The token is cached and refreshed before expiry.

<a name="KeycloakClient.HTTPClient"></a>
### func \(\*KeycloakClient\) [HTTPClient](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/auth/auth_client.go#L179>)

```go
func (c *KeycloakClient) HTTPClient() *http.Client
```

HTTPClient returns the underlying http.Client used by the KeycloakClient.

<a name="KeycloakConfig"></a>
## type [KeycloakConfig](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/auth/auth_client.go#L31-L34>)

KeycloakConfig holds the credentials and configuration details

```go
type KeycloakConfig struct {
AuthURL string
KeycloakRealm string
AuthURL string
Realm string
}
```

<a name="ResourceOwnerCredentials"></a>
## type [ResourceOwnerCredentials](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/auth/auth_client.go#L60-L64>)
## type [ResourceOwnerCredentials](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/auth/auth_client.go#L69-L73>)

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

Expand Down
10 changes: 10 additions & 0 deletions pkg/auth/auth_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,13 @@ func (c *KeycloakClient) requestToken(ctx context.Context) (*authResponse, error

return &authResp, nil
}

// HTTPClient returns the underlying http.Client used by the KeycloakClient.
func (c *KeycloakClient) HTTPClient() *http.Client {
return c.httpClient
}

// Config returns the KeycloakConfig used by the KeycloakClient.
func (c *KeycloakClient) Config() KeycloakConfig {
return c.cfg
}
89 changes: 89 additions & 0 deletions pkg/keycloakapi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<!-- gomarkdoc:embed:start -->

<!-- Code generated by gomarkdoc. DO NOT EDIT -->

# keycloakapi

```go
import "github.com/greenbone/opensight-golang-libraries/pkg/keycloakapi"
```

## Index

- [type Group](<#Group>)
- [type KeycloakAPIClient](<#KeycloakAPIClient>)
- [func NewKeycloakAPIClient\(authClient \*auth.KeycloakClient\) \*KeycloakAPIClient](<#NewKeycloakAPIClient>)
- [func \(kc \*KeycloakAPIClient\) ListGroups\(ctx context.Context\) \(\[\]Group, error\)](<#KeycloakAPIClient.ListGroups>)
- [func \(kc \*KeycloakAPIClient\) ListUsers\(ctx context.Context\) \(\[\]User, error\)](<#KeycloakAPIClient.ListUsers>)
- [type User](<#User>)


<a name="Group"></a>
## type [Group](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/keycloakapi/keycloak_api_client.go#L19-L22>)

Group represents a Keycloak group.

```go
type Group struct {
ID string `json:"id"`
Name string `json:"name"`
}
```

<a name="KeycloakAPIClient"></a>
## type [KeycloakAPIClient](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/keycloakapi/keycloak_api_client.go#L34-L36>)

KeycloakAPIClient provides methods to interact with Keycloak's REST API.

```go
type KeycloakAPIClient struct {
// contains filtered or unexported fields
}
```

<a name="NewKeycloakAPIClient"></a>
### func [NewKeycloakAPIClient](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/keycloakapi/keycloak_api_client.go#L39>)

```go
func NewKeycloakAPIClient(authClient *auth.KeycloakClient) *KeycloakAPIClient
```

NewKeycloakAPIClient creates a new KeycloakAPIClient.

<a name="KeycloakAPIClient.ListGroups"></a>
### func \(\*KeycloakAPIClient\) [ListGroups](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/keycloakapi/keycloak_api_client.go#L44>)

```go
func (kc *KeycloakAPIClient) ListGroups(ctx context.Context) ([]Group, error)
```

ListGroups retrieves all groups from Keycloak.

<a name="KeycloakAPIClient.ListUsers"></a>
### func \(\*KeycloakAPIClient\) [ListUsers](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/keycloakapi/keycloak_api_client.go#L81>)

```go
func (kc *KeycloakAPIClient) ListUsers(ctx context.Context) ([]User, error)
```

ListUsers retrieves all users from Keycloak.

<a name="User"></a>
## type [User](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/keycloakapi/keycloak_api_client.go#L25-L31>)

User represents a Keycloak user.

```go
type User struct {
ID string `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
}
```

Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)


<!-- gomarkdoc:embed:end -->
115 changes: 115 additions & 0 deletions pkg/keycloakapi/keycloak_api_client.go
Comment thread
ChellaVignesh marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// SPDX-FileCopyrightText: 2025 Greenbone AG <https://greenbone.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later

package keycloakapi

import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"

"github.com/greenbone/opensight-golang-libraries/pkg/auth"
)

// Group represents a Keycloak group.
type Group struct {
ID string `json:"id"`
Name string `json:"name"`
}

// User represents a Keycloak user.
type User struct {
ID string `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
}

// KeycloakAPIClient provides methods to interact with Keycloak's REST API.
type KeycloakAPIClient struct {
authClient *auth.KeycloakClient
}

// NewKeycloakAPIClient creates a new KeycloakAPIClient.
func NewKeycloakAPIClient(authClient *auth.KeycloakClient) *KeycloakAPIClient {
return &KeycloakAPIClient{authClient: authClient}
}

// ListGroups retrieves all groups from Keycloak.
func (kc *KeycloakAPIClient) ListGroups(ctx context.Context) ([]Group, error) {
token, err := kc.authClient.GetToken(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get token: %w", err)
}

config := kc.authClient.Config()
url := fmt.Sprintf("%s/admin/realms/%s/groups", config.AuthURL, config.Realm)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))

resp, err := kc.authClient.HTTPClient().Do(req)
if err != nil {
return nil, fmt.Errorf("failed to execute request: %w", err)
}
defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != http.StatusOK {
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to list users: %s: could not read response body: %w", resp.Status, err)
}

return nil, fmt.Errorf("failed to list users: %s: %s", resp.Status, strings.TrimSpace(string(body)))
}

var groups []Group
if err := json.NewDecoder(resp.Body).Decode(&groups); err != nil {
return nil, fmt.Errorf("failed to decode groups response: %w", err)
}
return groups, nil
}

// ListUsers retrieves all users from Keycloak.
func (kc *KeycloakAPIClient) ListUsers(ctx context.Context) ([]User, error) {
token, err := kc.authClient.GetToken(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get token: %w", err)
}

config := kc.authClient.Config()
url := fmt.Sprintf("%s/admin/realms/%s/users", config.AuthURL, config.Realm)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))

resp, err := kc.authClient.HTTPClient().Do(req)
if err != nil {
return nil, fmt.Errorf("failed to execute request: %w", err)
}
defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != http.StatusOK {
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to list users: %s: could not read response body: %w", resp.Status, err)
}

return nil, fmt.Errorf("failed to list users: %s: %s", resp.Status, strings.TrimSpace(string(body)))
}

var users []User
if err := json.NewDecoder(resp.Body).Decode(&users); err != nil {
return nil, fmt.Errorf("failed to decode users response: %w", err)
}
return users, nil
}
100 changes: 100 additions & 0 deletions pkg/keycloakapi/keycloak_api_client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// SPDX-FileCopyrightText: 2025 Greenbone AG <https://greenbone.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later

package keycloakapi

import (
"context"
"net/http"
"net/http/httptest"
"testing"

"github.com/greenbone/opensight-golang-libraries/pkg/auth"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func newTestKeycloakClient(serverURL string) *auth.KeycloakClient {
return auth.NewKeycloakClient(
&http.Client{},
auth.KeycloakConfig{
AuthURL: serverURL,
Realm: "test-realm",
},
auth.ClientCredentials{
ClientID: "test-client",
ClientSecret: "test-secret",
},
)
}

func TestListGroups(t *testing.T) {
groupsJSON := `[{"id":"1","name":"group1"},{"id":"2","name":"group2"}]`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/realms/test-realm/protocol/openid-connect/token" {
w.Header().Set("Content-Type", "application/json")
_, err := w.Write([]byte(`{"access_token":"test-token","expires_in":3600}`))
require.NoError(t, err)
return
}
if r.URL.Path == "/admin/realms/test-realm/groups" {
if r.Header.Get("Authorization") != "Bearer test-token" {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.Header().Set("Content-Type", "application/json")
_, err := w.Write([]byte(groupsJSON))
require.NoError(t, err)
return
}
w.WriteHeader(http.StatusNotFound)
}))
defer server.Close()

authClient := newTestKeycloakClient(server.URL)
apiClient := NewKeycloakAPIClient(authClient)
groups, err := apiClient.ListGroups(context.Background())
require.NoError(t, err, "expected no error")

wantGroups := []Group{
{ID: "1", Name: "group1"},
{ID: "2", Name: "group2"},
}
assert.ElementsMatch(t, wantGroups, groups, "groups mismatch")
}

func TestListUsers(t *testing.T) {
usersJSON := `[{"id":"1","username":"user1","email":"user1@example.com"},{"id":"2","username":"user2","email":"user2@example.com"}]`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/realms/test-realm/protocol/openid-connect/token" {
w.Header().Set("Content-Type", "application/json")
_, err := w.Write([]byte(`{"access_token":"test-token","expires_in":3600}`))
require.NoError(t, err)
return
}
if r.URL.Path == "/admin/realms/test-realm/users" {
if r.Header.Get("Authorization") != "Bearer test-token" {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.Header().Set("Content-Type", "application/json")
_, err := w.Write([]byte(usersJSON))
require.NoError(t, err)
return
}
w.WriteHeader(http.StatusNotFound)
}))
defer server.Close()

authClient := newTestKeycloakClient(server.URL)
apiClient := NewKeycloakAPIClient(authClient)
users, err := apiClient.ListUsers(context.Background())
require.NoError(t, err, "expected no error")

wantUsers := []User{
{ID: "1", Username: "user1", Email: "user1@example.com"},
{ID: "2", Username: "user2", Email: "user2@example.com"},
}
assert.ElementsMatch(t, wantUsers, users, "users mismatch")
}
Loading