feat(cache): implement hashed API key storage and retrieval

Adds a new hashed key storage mechanism for API keys in the cache. 
Replaces direct mapping to API keys with composite keys based on 
organizationId and name. Implements searching of API keys using 
hash comparisons for improved security. Updates related tests to 
ensure correct functionality and validate the hashing. Also, 
adds support for a new dependency `golang.org/x/crypto`.
This commit is contained in:
2025-11-20 22:11:17 +01:00
parent 1e2236dc9e
commit 4468903535
11 changed files with 357 additions and 69 deletions
+2 -3
View File
@@ -30,7 +30,6 @@ import (
"gitlab.com/unboundsoftware/schemas/domain"
"gitlab.com/unboundsoftware/schemas/graph"
"gitlab.com/unboundsoftware/schemas/graph/generated"
"gitlab.com/unboundsoftware/schemas/hash"
"gitlab.com/unboundsoftware/schemas/logging"
"gitlab.com/unboundsoftware/schemas/middleware"
"gitlab.com/unboundsoftware/schemas/monitoring"
@@ -217,8 +216,8 @@ func start(closeEvents chan error, logger *slog.Logger, connectToAmqpFunc func(u
logger.Info("WebSocket connection with API key", "has_key", true)
ctx = context.WithValue(ctx, middleware.ApiKey, apiKey)
// Look up organization by API key (same logic as auth middleware)
if organization := serviceCache.OrganizationByAPIKey(hash.String(apiKey)); organization != nil {
// Look up organization by API key (cache handles hash comparison)
if organization := serviceCache.OrganizationByAPIKey(apiKey); organization != nil {
logger.Info("WebSocket: Organization found for API key", "org_id", organization.ID.String())
ctx = context.WithValue(ctx, middleware.OrganizationKey, *organization)
} else {
+47 -21
View File
@@ -17,11 +17,18 @@ import (
// MockCache is a mock implementation for testing
type MockCache struct {
organizations map[string]*domain.Organization
organizations map[string]*domain.Organization // keyed by orgId-name composite
apiKeys map[string]string // maps orgId-name to hashed key
}
func (m *MockCache) OrganizationByAPIKey(apiKey string) *domain.Organization {
return m.organizations[apiKey]
func (m *MockCache) OrganizationByAPIKey(plainKey string) *domain.Organization {
// Find organization by comparing plaintext key with stored hash
for compositeKey, hashedKey := range m.apiKeys {
if hash.CompareAPIKey(hashedKey, plainKey) {
return m.organizations[compositeKey]
}
}
return nil
}
func TestWebSocketInitFunc_WithValidAPIKey(t *testing.T) {
@@ -35,11 +42,17 @@ func TestWebSocketInitFunc_WithValidAPIKey(t *testing.T) {
}
apiKey := "test-api-key-123"
hashedKey := hash.String(apiKey)
hashedKey, err := hash.APIKey(apiKey)
require.NoError(t, err)
compositeKey := orgID.String() + "-test-key"
mockCache := &MockCache{
organizations: map[string]*domain.Organization{
hashedKey: org,
compositeKey: org,
},
apiKeys: map[string]string{
compositeKey: hashedKey,
},
}
@@ -49,8 +62,8 @@ func TestWebSocketInitFunc_WithValidAPIKey(t *testing.T) {
if apiKey, ok := initPayload["X-Api-Key"].(string); ok && apiKey != "" {
ctx = context.WithValue(ctx, middleware.ApiKey, apiKey)
// Look up organization by API key
if organization := mockCache.OrganizationByAPIKey(hash.String(apiKey)); organization != nil {
// Look up organization by API key (cache handles hash comparison)
if organization := mockCache.OrganizationByAPIKey(apiKey); organization != nil {
ctx = context.WithValue(ctx, middleware.OrganizationKey, *organization)
}
}
@@ -91,6 +104,7 @@ func TestWebSocketInitFunc_WithInvalidAPIKey(t *testing.T) {
// Setup
mockCache := &MockCache{
organizations: map[string]*domain.Organization{},
apiKeys: map[string]string{},
}
apiKey := "invalid-api-key"
@@ -101,8 +115,8 @@ func TestWebSocketInitFunc_WithInvalidAPIKey(t *testing.T) {
if apiKey, ok := initPayload["X-Api-Key"].(string); ok && apiKey != "" {
ctx = context.WithValue(ctx, middleware.ApiKey, apiKey)
// Look up organization by API key
if organization := mockCache.OrganizationByAPIKey(hash.String(apiKey)); organization != nil {
// Look up organization by API key (cache handles hash comparison)
if organization := mockCache.OrganizationByAPIKey(apiKey); organization != nil {
ctx = context.WithValue(ctx, middleware.OrganizationKey, *organization)
}
}
@@ -137,6 +151,7 @@ func TestWebSocketInitFunc_WithoutAPIKey(t *testing.T) {
// Setup
mockCache := &MockCache{
organizations: map[string]*domain.Organization{},
apiKeys: map[string]string{},
}
// Create InitFunc
@@ -145,8 +160,8 @@ func TestWebSocketInitFunc_WithoutAPIKey(t *testing.T) {
if apiKey, ok := initPayload["X-Api-Key"].(string); ok && apiKey != "" {
ctx = context.WithValue(ctx, middleware.ApiKey, apiKey)
// Look up organization by API key
if organization := mockCache.OrganizationByAPIKey(hash.String(apiKey)); organization != nil {
// Look up organization by API key (cache handles hash comparison)
if organization := mockCache.OrganizationByAPIKey(apiKey); organization != nil {
ctx = context.WithValue(ctx, middleware.OrganizationKey, *organization)
}
}
@@ -176,6 +191,7 @@ func TestWebSocketInitFunc_WithEmptyAPIKey(t *testing.T) {
// Setup
mockCache := &MockCache{
organizations: map[string]*domain.Organization{},
apiKeys: map[string]string{},
}
// Create InitFunc
@@ -184,8 +200,8 @@ func TestWebSocketInitFunc_WithEmptyAPIKey(t *testing.T) {
if apiKey, ok := initPayload["X-Api-Key"].(string); ok && apiKey != "" {
ctx = context.WithValue(ctx, middleware.ApiKey, apiKey)
// Look up organization by API key
if organization := mockCache.OrganizationByAPIKey(hash.String(apiKey)); organization != nil {
// Look up organization by API key (cache handles hash comparison)
if organization := mockCache.OrganizationByAPIKey(apiKey); organization != nil {
ctx = context.WithValue(ctx, middleware.OrganizationKey, *organization)
}
}
@@ -217,6 +233,7 @@ func TestWebSocketInitFunc_WithWrongTypeAPIKey(t *testing.T) {
// Setup
mockCache := &MockCache{
organizations: map[string]*domain.Organization{},
apiKeys: map[string]string{},
}
// Create InitFunc
@@ -225,8 +242,8 @@ func TestWebSocketInitFunc_WithWrongTypeAPIKey(t *testing.T) {
if apiKey, ok := initPayload["X-Api-Key"].(string); ok && apiKey != "" {
ctx = context.WithValue(ctx, middleware.ApiKey, apiKey)
// Look up organization by API key
if organization := mockCache.OrganizationByAPIKey(hash.String(apiKey)); organization != nil {
// Look up organization by API key (cache handles hash comparison)
if organization := mockCache.OrganizationByAPIKey(apiKey); organization != nil {
ctx = context.WithValue(ctx, middleware.OrganizationKey, *organization)
}
}
@@ -274,13 +291,22 @@ func TestWebSocketInitFunc_WithMultipleOrganizations(t *testing.T) {
apiKey1 := "api-key-org-1"
apiKey2 := "api-key-org-2"
hashedKey1 := hash.String(apiKey1)
hashedKey2 := hash.String(apiKey2)
hashedKey1, err := hash.APIKey(apiKey1)
require.NoError(t, err)
hashedKey2, err := hash.APIKey(apiKey2)
require.NoError(t, err)
compositeKey1 := org1ID.String() + "-key1"
compositeKey2 := org2ID.String() + "-key2"
mockCache := &MockCache{
organizations: map[string]*domain.Organization{
hashedKey1: org1,
hashedKey2: org2,
compositeKey1: org1,
compositeKey2: org2,
},
apiKeys: map[string]string{
compositeKey1: hashedKey1,
compositeKey2: hashedKey2,
},
}
@@ -290,8 +316,8 @@ func TestWebSocketInitFunc_WithMultipleOrganizations(t *testing.T) {
if apiKey, ok := initPayload["X-Api-Key"].(string); ok && apiKey != "" {
ctx = context.WithValue(ctx, middleware.ApiKey, apiKey)
// Look up organization by API key
if organization := mockCache.OrganizationByAPIKey(hash.String(apiKey)); organization != nil {
// Look up organization by API key (cache handles hash comparison)
if organization := mockCache.OrganizationByAPIKey(apiKey); organization != nil {
ctx = context.WithValue(ctx, middleware.OrganizationKey, *organization)
}
}