feat: add commands for managing organizations and users
Introduce `AddUserToOrganization`, `RemoveAPIKey`, and `RemoveOrganization` commands to enhance organization management. Implement validation for user addition and API key removal. Update GraphQL schema to support new mutations and add caching for the new events, ensuring that organizations and their relationships are accurately represented in the cache.
This commit is contained in:
Vendored
+78
@@ -53,6 +53,17 @@ func (c *Cache) OrganizationsByUser(sub string) []domain.Organization {
|
||||
return orgs
|
||||
}
|
||||
|
||||
func (c *Cache) AllOrganizations() []domain.Organization {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
orgs := make([]domain.Organization, 0, len(c.organizations))
|
||||
for _, org := range c.organizations {
|
||||
orgs = append(orgs, org)
|
||||
}
|
||||
return orgs
|
||||
}
|
||||
|
||||
func (c *Cache) ApiKeyByKey(key string) *domain.APIKey {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
@@ -100,6 +111,16 @@ func (c *Cache) Update(msg any, _ goamqp.Headers) (any, error) {
|
||||
c.organizations[m.ID.String()] = o
|
||||
c.addUser(m.Initiator, o)
|
||||
c.logger.With("org_id", m.ID.String(), "event", "OrganizationAdded").Debug("cache updated")
|
||||
case *domain.UserAddedToOrganization:
|
||||
org, exists := c.organizations[m.ID.String()]
|
||||
if exists {
|
||||
m.UpdateOrganization(&org)
|
||||
c.organizations[m.ID.String()] = org
|
||||
c.addUser(m.UserId, org)
|
||||
c.logger.With("org_id", m.ID.String(), "user_id", m.UserId, "event", "UserAddedToOrganization").Debug("cache updated")
|
||||
} else {
|
||||
c.logger.With("org_id", m.ID.String(), "event", "UserAddedToOrganization").Warn("organization not found in cache")
|
||||
}
|
||||
case *domain.APIKeyAdded:
|
||||
key := domain.APIKey{
|
||||
Name: m.Name,
|
||||
@@ -117,6 +138,63 @@ func (c *Cache) Update(msg any, _ goamqp.Headers) (any, error) {
|
||||
org.APIKeys = append(org.APIKeys, key)
|
||||
c.organizations[m.OrganizationId] = org
|
||||
c.logger.With("org_id", m.OrganizationId, "key_name", m.Name, "event", "APIKeyAdded").Debug("cache updated")
|
||||
case *domain.APIKeyRemoved:
|
||||
orgId := m.ID.String()
|
||||
org, exists := c.organizations[orgId]
|
||||
if exists {
|
||||
// Remove from organization's API keys list
|
||||
for i, key := range org.APIKeys {
|
||||
if key.Name == m.KeyName {
|
||||
org.APIKeys = append(org.APIKeys[:i], org.APIKeys[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
c.organizations[orgId] = org
|
||||
// Remove from apiKeys map
|
||||
delete(c.apiKeys, apiKeyId(orgId, m.KeyName))
|
||||
c.logger.With("org_id", orgId, "key_name", m.KeyName, "event", "APIKeyRemoved").Debug("cache updated")
|
||||
} else {
|
||||
c.logger.With("org_id", orgId, "event", "APIKeyRemoved").Warn("organization not found in cache")
|
||||
}
|
||||
case *domain.OrganizationRemoved:
|
||||
orgId := m.ID.String()
|
||||
org, exists := c.organizations[orgId]
|
||||
if exists {
|
||||
// Remove all API keys for this organization
|
||||
for _, key := range org.APIKeys {
|
||||
delete(c.apiKeys, apiKeyId(orgId, key.Name))
|
||||
}
|
||||
// Remove organization from all users
|
||||
for userId, userOrgs := range c.users {
|
||||
for i, userOrgId := range userOrgs {
|
||||
if userOrgId == orgId {
|
||||
c.users[userId] = append(userOrgs[:i], userOrgs[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
// If user has no more organizations, remove from map
|
||||
if len(c.users[userId]) == 0 {
|
||||
delete(c.users, userId)
|
||||
}
|
||||
}
|
||||
// Remove services for this organization
|
||||
if refs, exists := c.services[orgId]; exists {
|
||||
for ref := range refs {
|
||||
// Remove all subgraphs for this org/ref combination
|
||||
for service := range refs[ref] {
|
||||
delete(c.subGraphs, subGraphKey(orgId, ref, service))
|
||||
}
|
||||
// Remove lastUpdate for this org/ref
|
||||
delete(c.lastUpdate, refKey(orgId, ref))
|
||||
}
|
||||
delete(c.services, orgId)
|
||||
}
|
||||
// Remove organization
|
||||
delete(c.organizations, orgId)
|
||||
c.logger.With("org_id", orgId, "event", "OrganizationRemoved").Debug("cache updated")
|
||||
} else {
|
||||
c.logger.With("org_id", orgId, "event", "OrganizationRemoved").Warn("organization not found in cache")
|
||||
}
|
||||
case *domain.SubGraphUpdated:
|
||||
c.updateSubGraph(m.OrganizationId, m.Ref, m.ID.String(), m.Service, m.Time)
|
||||
c.logger.With("org_id", m.OrganizationId, "ref", m.Ref, "service", m.Service, "event", "SubGraphUpdated").Debug("cache updated")
|
||||
|
||||
Vendored
+210
@@ -445,3 +445,213 @@ func TestCache_ConcurrentReadsAndWrites(t *testing.T) {
|
||||
// Verify cache is in consistent state
|
||||
assert.GreaterOrEqual(t, len(c.organizations), numWriters)
|
||||
}
|
||||
|
||||
func TestCache_Update_APIKeyRemoved(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
c := New(logger)
|
||||
|
||||
orgID := uuid.New().String()
|
||||
keyName := "test-key"
|
||||
hashedKey := "hashed-key-value"
|
||||
|
||||
// Add organization with API key
|
||||
org := domain.Organization{
|
||||
BaseAggregate: eventsourced.BaseAggregateFromString(orgID),
|
||||
Name: "Test Org",
|
||||
APIKeys: []domain.APIKey{
|
||||
{
|
||||
Name: keyName,
|
||||
OrganizationId: orgID,
|
||||
Key: hashedKey,
|
||||
Refs: []string{"main"},
|
||||
Read: true,
|
||||
Publish: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
c.organizations[orgID] = org
|
||||
c.apiKeys[apiKeyId(orgID, keyName)] = org.APIKeys[0]
|
||||
|
||||
// Verify key exists before removal
|
||||
_, exists := c.apiKeys[apiKeyId(orgID, keyName)]
|
||||
assert.True(t, exists)
|
||||
|
||||
// Remove the API key
|
||||
event := &domain.APIKeyRemoved{
|
||||
KeyName: keyName,
|
||||
Initiator: "user-123",
|
||||
}
|
||||
event.ID = *eventsourced.IdFromString(orgID)
|
||||
|
||||
_, err := c.Update(event, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify API key was removed from cache
|
||||
_, exists = c.apiKeys[apiKeyId(orgID, keyName)]
|
||||
assert.False(t, exists, "API key should be removed from cache")
|
||||
|
||||
// Verify API key was removed from organization
|
||||
updatedOrg := c.organizations[orgID]
|
||||
assert.Len(t, updatedOrg.APIKeys, 0, "API key should be removed from organization")
|
||||
}
|
||||
|
||||
func TestCache_Update_APIKeyRemoved_MultipleKeys(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
c := New(logger)
|
||||
|
||||
orgID := uuid.New().String()
|
||||
|
||||
// Add organization with multiple API keys
|
||||
org := domain.Organization{
|
||||
BaseAggregate: eventsourced.BaseAggregateFromString(orgID),
|
||||
Name: "Test Org",
|
||||
APIKeys: []domain.APIKey{
|
||||
{
|
||||
Name: "key1",
|
||||
OrganizationId: orgID,
|
||||
Key: "hash1",
|
||||
},
|
||||
{
|
||||
Name: "key2",
|
||||
OrganizationId: orgID,
|
||||
Key: "hash2",
|
||||
},
|
||||
{
|
||||
Name: "key3",
|
||||
OrganizationId: orgID,
|
||||
Key: "hash3",
|
||||
},
|
||||
},
|
||||
}
|
||||
c.organizations[orgID] = org
|
||||
c.apiKeys[apiKeyId(orgID, "key1")] = org.APIKeys[0]
|
||||
c.apiKeys[apiKeyId(orgID, "key2")] = org.APIKeys[1]
|
||||
c.apiKeys[apiKeyId(orgID, "key3")] = org.APIKeys[2]
|
||||
|
||||
// Remove the middle key
|
||||
event := &domain.APIKeyRemoved{
|
||||
KeyName: "key2",
|
||||
Initiator: "user-123",
|
||||
}
|
||||
event.ID = *eventsourced.IdFromString(orgID)
|
||||
|
||||
_, err := c.Update(event, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify only key2 was removed
|
||||
_, exists := c.apiKeys[apiKeyId(orgID, "key1")]
|
||||
assert.True(t, exists, "key1 should still exist")
|
||||
|
||||
_, exists = c.apiKeys[apiKeyId(orgID, "key2")]
|
||||
assert.False(t, exists, "key2 should be removed")
|
||||
|
||||
_, exists = c.apiKeys[apiKeyId(orgID, "key3")]
|
||||
assert.True(t, exists, "key3 should still exist")
|
||||
|
||||
// Verify organization has 2 keys remaining
|
||||
updatedOrg := c.organizations[orgID]
|
||||
assert.Len(t, updatedOrg.APIKeys, 2)
|
||||
assert.Equal(t, "key1", updatedOrg.APIKeys[0].Name)
|
||||
assert.Equal(t, "key3", updatedOrg.APIKeys[1].Name)
|
||||
}
|
||||
|
||||
func TestCache_Update_OrganizationRemoved(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
c := New(logger)
|
||||
|
||||
orgID := uuid.New().String()
|
||||
userSub := "user-123"
|
||||
|
||||
// Add organization with API keys, users, and subgraphs
|
||||
org := domain.Organization{
|
||||
BaseAggregate: eventsourced.BaseAggregateFromString(orgID),
|
||||
Name: "Test Org",
|
||||
APIKeys: []domain.APIKey{
|
||||
{
|
||||
Name: "key1",
|
||||
OrganizationId: orgID,
|
||||
Key: "hash1",
|
||||
},
|
||||
},
|
||||
}
|
||||
c.organizations[orgID] = org
|
||||
c.apiKeys[apiKeyId(orgID, "key1")] = org.APIKeys[0]
|
||||
c.users[userSub] = []string{orgID}
|
||||
c.services[orgID] = map[string]map[string]struct{}{
|
||||
"main": {
|
||||
"service1": {},
|
||||
},
|
||||
}
|
||||
c.subGraphs[subGraphKey(orgID, "main", "service1")] = "subgraph-id"
|
||||
c.lastUpdate[refKey(orgID, "main")] = "2024-01-01T12:00:00Z"
|
||||
|
||||
// Remove the organization
|
||||
event := &domain.OrganizationRemoved{
|
||||
Initiator: userSub,
|
||||
}
|
||||
event.ID = *eventsourced.IdFromString(orgID)
|
||||
|
||||
_, err := c.Update(event, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify organization was removed
|
||||
_, exists := c.organizations[orgID]
|
||||
assert.False(t, exists, "Organization should be removed from cache")
|
||||
|
||||
// Verify API keys were removed
|
||||
_, exists = c.apiKeys[apiKeyId(orgID, "key1")]
|
||||
assert.False(t, exists, "API keys should be removed from cache")
|
||||
|
||||
// Verify user association was removed
|
||||
userOrgs := c.users[userSub]
|
||||
assert.NotContains(t, userOrgs, orgID, "User should not be associated with removed organization")
|
||||
|
||||
// Verify services were removed
|
||||
_, exists = c.services[orgID]
|
||||
assert.False(t, exists, "Services should be removed from cache")
|
||||
|
||||
// Verify subgraphs were removed
|
||||
_, exists = c.subGraphs[subGraphKey(orgID, "main", "service1")]
|
||||
assert.False(t, exists, "Subgraphs should be removed from cache")
|
||||
|
||||
// Verify lastUpdate was removed
|
||||
_, exists = c.lastUpdate[refKey(orgID, "main")]
|
||||
assert.False(t, exists, "LastUpdate should be removed from cache")
|
||||
}
|
||||
|
||||
func TestCache_Update_OrganizationRemoved_MultipleUsers(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
c := New(logger)
|
||||
|
||||
orgID := uuid.New().String()
|
||||
user1 := "user-1"
|
||||
user2 := "user-2"
|
||||
otherOrgID := uuid.New().String()
|
||||
|
||||
// Add organization
|
||||
org := domain.Organization{
|
||||
BaseAggregate: eventsourced.BaseAggregateFromString(orgID),
|
||||
Name: "Test Org",
|
||||
}
|
||||
c.organizations[orgID] = org
|
||||
|
||||
// Add users with multiple org associations
|
||||
c.users[user1] = []string{orgID, otherOrgID}
|
||||
c.users[user2] = []string{orgID}
|
||||
|
||||
// Remove the organization
|
||||
event := &domain.OrganizationRemoved{
|
||||
Initiator: user1,
|
||||
}
|
||||
event.ID = *eventsourced.IdFromString(orgID)
|
||||
|
||||
_, err := c.Update(event, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify user1 still has otherOrgID but not removed orgID
|
||||
assert.Len(t, c.users[user1], 1)
|
||||
assert.Equal(t, otherOrgID, c.users[user1][0])
|
||||
|
||||
// Verify user2 has no organizations
|
||||
assert.Len(t, c.users[user2], 0)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user