Merge branch 'feat/manage-organizations-users' into 'main'
feat: add commands for managing organizations and users See merge request unboundsoftware/schemas!637
This commit was merged in pull request #641.
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)
|
||||
}
|
||||
|
||||
@@ -92,7 +92,10 @@ func start(closeEvents chan error, logger *slog.Logger, connectToAmqpFunc func(u
|
||||
pg.WithEventTypes(
|
||||
&domain.SubGraphUpdated{},
|
||||
&domain.OrganizationAdded{},
|
||||
&domain.UserAddedToOrganization{},
|
||||
&domain.APIKeyAdded{},
|
||||
&domain.APIKeyRemoved{},
|
||||
&domain.OrganizationRemoved{},
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
@@ -127,10 +130,16 @@ func start(closeEvents chan error, logger *slog.Logger, connectToAmqpFunc func(u
|
||||
goamqp.EventStreamPublisher(publisher),
|
||||
goamqp.TransientEventStreamConsumer("SubGraph.Updated", serviceCache.Update, domain.SubGraphUpdated{}),
|
||||
goamqp.TransientEventStreamConsumer("Organization.Added", serviceCache.Update, domain.OrganizationAdded{}),
|
||||
goamqp.TransientEventStreamConsumer("Organization.UserAdded", serviceCache.Update, domain.UserAddedToOrganization{}),
|
||||
goamqp.TransientEventStreamConsumer("Organization.APIKeyAdded", serviceCache.Update, domain.APIKeyAdded{}),
|
||||
goamqp.TransientEventStreamConsumer("Organization.APIKeyRemoved", serviceCache.Update, domain.APIKeyRemoved{}),
|
||||
goamqp.TransientEventStreamConsumer("Organization.Removed", serviceCache.Update, domain.OrganizationRemoved{}),
|
||||
goamqp.WithTypeMapping("SubGraph.Updated", domain.SubGraphUpdated{}),
|
||||
goamqp.WithTypeMapping("Organization.Added", domain.OrganizationAdded{}),
|
||||
goamqp.WithTypeMapping("Organization.UserAdded", domain.UserAddedToOrganization{}),
|
||||
goamqp.WithTypeMapping("Organization.APIKeyAdded", domain.APIKeyAdded{}),
|
||||
goamqp.WithTypeMapping("Organization.APIKeyRemoved", domain.APIKeyRemoved{}),
|
||||
goamqp.WithTypeMapping("Organization.Removed", domain.OrganizationRemoved{}),
|
||||
}
|
||||
if err := conn.Start(rootCtx, setups...); err != nil {
|
||||
return fmt.Errorf("failed to setup AMQP: %v", err)
|
||||
|
||||
@@ -23,6 +23,8 @@ func (o *Organization) Apply(event eventsourced.Event) error {
|
||||
switch e := event.(type) {
|
||||
case *OrganizationAdded:
|
||||
e.UpdateOrganization(o)
|
||||
case *UserAddedToOrganization:
|
||||
e.UpdateOrganization(o)
|
||||
case *APIKeyAdded:
|
||||
o.APIKeys = append(o.APIKeys, APIKey{
|
||||
Name: e.Name,
|
||||
@@ -36,6 +38,10 @@ func (o *Organization) Apply(event eventsourced.Event) error {
|
||||
})
|
||||
o.ChangedBy = e.Initiator
|
||||
o.ChangedAt = e.When()
|
||||
case *APIKeyRemoved:
|
||||
e.UpdateOrganization(o)
|
||||
case *OrganizationRemoved:
|
||||
e.UpdateOrganization(o)
|
||||
default:
|
||||
return fmt.Errorf("unexpected event type: %+v", event)
|
||||
}
|
||||
|
||||
@@ -34,6 +34,37 @@ func (a AddOrganization) Event(context.Context) eventsourced.Event {
|
||||
|
||||
var _ eventsourced.Command = AddOrganization{}
|
||||
|
||||
type AddUserToOrganization struct {
|
||||
UserId string
|
||||
Initiator string
|
||||
}
|
||||
|
||||
func (a AddUserToOrganization) Validate(_ context.Context, aggregate eventsourced.Aggregate) error {
|
||||
if aggregate.Identity() == nil {
|
||||
return fmt.Errorf("organization does not exist")
|
||||
}
|
||||
if len(a.UserId) == 0 {
|
||||
return fmt.Errorf("userId is required")
|
||||
}
|
||||
// Check if user is already in the organization
|
||||
org := aggregate.(*Organization)
|
||||
for _, user := range org.Users {
|
||||
if user == a.UserId {
|
||||
return fmt.Errorf("user is already a member of this organization")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a AddUserToOrganization) Event(context.Context) eventsourced.Event {
|
||||
return &UserAddedToOrganization{
|
||||
UserId: a.UserId,
|
||||
Initiator: a.Initiator,
|
||||
}
|
||||
}
|
||||
|
||||
var _ eventsourced.Command = AddUserToOrganization{}
|
||||
|
||||
type AddAPIKey struct {
|
||||
Name string
|
||||
Key string
|
||||
@@ -79,6 +110,57 @@ func (a AddAPIKey) Event(context.Context) eventsourced.Event {
|
||||
|
||||
var _ eventsourced.Command = AddAPIKey{}
|
||||
|
||||
type RemoveAPIKey struct {
|
||||
KeyName string
|
||||
Initiator string
|
||||
}
|
||||
|
||||
func (r RemoveAPIKey) Validate(_ context.Context, aggregate eventsourced.Aggregate) error {
|
||||
if aggregate.Identity() == nil {
|
||||
return fmt.Errorf("organization does not exist")
|
||||
}
|
||||
org := aggregate.(*Organization)
|
||||
found := false
|
||||
for _, k := range org.APIKeys {
|
||||
if k.Name == r.KeyName {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("API key '%s' not found", r.KeyName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r RemoveAPIKey) Event(context.Context) eventsourced.Event {
|
||||
return &APIKeyRemoved{
|
||||
KeyName: r.KeyName,
|
||||
Initiator: r.Initiator,
|
||||
}
|
||||
}
|
||||
|
||||
var _ eventsourced.Command = RemoveAPIKey{}
|
||||
|
||||
type RemoveOrganization struct {
|
||||
Initiator string
|
||||
}
|
||||
|
||||
func (r RemoveOrganization) Validate(_ context.Context, aggregate eventsourced.Aggregate) error {
|
||||
if aggregate.Identity() == nil {
|
||||
return fmt.Errorf("organization does not exist")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r RemoveOrganization) Event(context.Context) eventsourced.Event {
|
||||
return &OrganizationRemoved{
|
||||
Initiator: r.Initiator,
|
||||
}
|
||||
}
|
||||
|
||||
var _ eventsourced.Command = RemoveOrganization{}
|
||||
|
||||
type UpdateSubGraph struct {
|
||||
OrganizationId string
|
||||
Ref string
|
||||
|
||||
@@ -464,3 +464,114 @@ func TestUpdateSubGraph_Event(t *testing.T) {
|
||||
assert.Equal(t, "type Query { hello: String }", subGraphEvent.Sdl)
|
||||
assert.Equal(t, "user@example.com", subGraphEvent.Initiator)
|
||||
}
|
||||
|
||||
// RemoveAPIKey tests
|
||||
|
||||
func TestRemoveAPIKey_Validate_Success(t *testing.T) {
|
||||
cmd := RemoveAPIKey{
|
||||
KeyName: "production-key",
|
||||
Initiator: "user@example.com",
|
||||
}
|
||||
|
||||
org := &Organization{
|
||||
BaseAggregate: eventsourced.BaseAggregateFromString("org-123"),
|
||||
APIKeys: []APIKey{
|
||||
{
|
||||
Name: "production-key",
|
||||
Key: "hashed-key",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := cmd.Validate(context.Background(), org)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestRemoveAPIKey_Validate_OrganizationNotExists(t *testing.T) {
|
||||
cmd := RemoveAPIKey{
|
||||
KeyName: "production-key",
|
||||
Initiator: "user@example.com",
|
||||
}
|
||||
|
||||
org := &Organization{} // No identity means it doesn't exist
|
||||
err := cmd.Validate(context.Background(), org)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "does not exist")
|
||||
}
|
||||
|
||||
func TestRemoveAPIKey_Validate_KeyNotFound(t *testing.T) {
|
||||
cmd := RemoveAPIKey{
|
||||
KeyName: "non-existent-key",
|
||||
Initiator: "user@example.com",
|
||||
}
|
||||
|
||||
org := &Organization{
|
||||
BaseAggregate: eventsourced.BaseAggregateFromString("org-123"),
|
||||
APIKeys: []APIKey{
|
||||
{
|
||||
Name: "production-key",
|
||||
Key: "hashed-key",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := cmd.Validate(context.Background(), org)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not found")
|
||||
assert.Contains(t, err.Error(), "non-existent-key")
|
||||
}
|
||||
|
||||
func TestRemoveAPIKey_Event(t *testing.T) {
|
||||
cmd := RemoveAPIKey{
|
||||
KeyName: "production-key",
|
||||
Initiator: "user@example.com",
|
||||
}
|
||||
|
||||
event := cmd.Event(context.Background())
|
||||
require.NotNil(t, event)
|
||||
|
||||
keyEvent, ok := event.(*APIKeyRemoved)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "production-key", keyEvent.KeyName)
|
||||
assert.Equal(t, "user@example.com", keyEvent.Initiator)
|
||||
}
|
||||
|
||||
// RemoveOrganization tests
|
||||
|
||||
func TestRemoveOrganization_Validate_Success(t *testing.T) {
|
||||
cmd := RemoveOrganization{
|
||||
Initiator: "user@example.com",
|
||||
}
|
||||
|
||||
org := &Organization{
|
||||
BaseAggregate: eventsourced.BaseAggregateFromString("org-123"),
|
||||
Name: "Test Org",
|
||||
}
|
||||
|
||||
err := cmd.Validate(context.Background(), org)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestRemoveOrganization_Validate_OrganizationNotExists(t *testing.T) {
|
||||
cmd := RemoveOrganization{
|
||||
Initiator: "user@example.com",
|
||||
}
|
||||
|
||||
org := &Organization{} // No identity means it doesn't exist
|
||||
err := cmd.Validate(context.Background(), org)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "does not exist")
|
||||
}
|
||||
|
||||
func TestRemoveOrganization_Event(t *testing.T) {
|
||||
cmd := RemoveOrganization{
|
||||
Initiator: "user@example.com",
|
||||
}
|
||||
|
||||
event := cmd.Event(context.Background())
|
||||
require.NotNil(t, event)
|
||||
|
||||
orgEvent, ok := event.(*OrganizationRemoved)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "user@example.com", orgEvent.Initiator)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,24 @@ func (a *OrganizationAdded) UpdateOrganization(o *Organization) {
|
||||
o.ChangedAt = a.When()
|
||||
}
|
||||
|
||||
type UserAddedToOrganization struct {
|
||||
eventsourced.BaseEvent
|
||||
UserId string `json:"userId"`
|
||||
Initiator string `json:"initiator"`
|
||||
}
|
||||
|
||||
func (a *UserAddedToOrganization) UpdateOrganization(o *Organization) {
|
||||
// Check if user is already in the organization
|
||||
for _, user := range o.Users {
|
||||
if user == a.UserId {
|
||||
return // User already exists, no need to add
|
||||
}
|
||||
}
|
||||
o.Users = append(o.Users, a.UserId)
|
||||
o.ChangedBy = a.Initiator
|
||||
o.ChangedAt = a.When()
|
||||
}
|
||||
|
||||
type APIKeyAdded struct {
|
||||
eventsourced.BaseEvent
|
||||
OrganizationId string `json:"organizationId"`
|
||||
@@ -34,6 +52,36 @@ func (a *APIKeyAdded) EnrichFromAggregate(aggregate eventsourced.Aggregate) {
|
||||
|
||||
var _ eventsourced.EnrichableEvent = &APIKeyAdded{}
|
||||
|
||||
type APIKeyRemoved struct {
|
||||
eventsourced.BaseEvent
|
||||
KeyName string `json:"keyName"`
|
||||
Initiator string `json:"initiator"`
|
||||
}
|
||||
|
||||
func (a *APIKeyRemoved) UpdateOrganization(o *Organization) {
|
||||
// Remove the API key from the organization
|
||||
for i, key := range o.APIKeys {
|
||||
if key.Name == a.KeyName {
|
||||
o.APIKeys = append(o.APIKeys[:i], o.APIKeys[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
o.ChangedBy = a.Initiator
|
||||
o.ChangedAt = a.When()
|
||||
}
|
||||
|
||||
type OrganizationRemoved struct {
|
||||
eventsourced.BaseEvent
|
||||
Initiator string `json:"initiator"`
|
||||
}
|
||||
|
||||
func (a *OrganizationRemoved) UpdateOrganization(o *Organization) {
|
||||
// Mark organization as removed by clearing critical fields
|
||||
// The aggregate will still exist in the event store, but it's logically deleted
|
||||
o.ChangedBy = a.Initiator
|
||||
o.ChangedAt = a.When()
|
||||
}
|
||||
|
||||
type SubGraphUpdated struct {
|
||||
eventsourced.BaseEvent
|
||||
OrganizationId string `json:"organizationId"`
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
|
||||
)
|
||||
|
||||
func TestOrganizationAdded_UpdateOrganization(t *testing.T) {
|
||||
event := &OrganizationAdded{
|
||||
BaseEvent: eventsourced.BaseEvent{
|
||||
EventTime: eventsourced.EventTime{
|
||||
Time: time.Now(),
|
||||
},
|
||||
},
|
||||
Name: "Test Organization",
|
||||
Initiator: "user@example.com",
|
||||
}
|
||||
|
||||
org := &Organization{
|
||||
BaseAggregate: eventsourced.BaseAggregateFromString("org-123"),
|
||||
}
|
||||
|
||||
event.UpdateOrganization(org)
|
||||
|
||||
assert.Equal(t, "Test Organization", org.Name)
|
||||
assert.Equal(t, []string{"user@example.com"}, org.Users)
|
||||
assert.Equal(t, "user@example.com", org.CreatedBy)
|
||||
assert.Equal(t, "user@example.com", org.ChangedBy)
|
||||
assert.Equal(t, event.When(), org.CreatedAt)
|
||||
assert.Equal(t, event.When(), org.ChangedAt)
|
||||
}
|
||||
|
||||
func TestUserAddedToOrganization_UpdateOrganization(t *testing.T) {
|
||||
event := &UserAddedToOrganization{
|
||||
BaseEvent: eventsourced.BaseEvent{
|
||||
EventTime: eventsourced.EventTime{
|
||||
Time: time.Now(),
|
||||
},
|
||||
},
|
||||
UserId: "new-user@example.com",
|
||||
Initiator: "admin@example.com",
|
||||
}
|
||||
|
||||
org := &Organization{
|
||||
BaseAggregate: eventsourced.BaseAggregateFromString("org-123"),
|
||||
Users: []string{"existing-user@example.com"},
|
||||
}
|
||||
|
||||
event.UpdateOrganization(org)
|
||||
|
||||
assert.Len(t, org.Users, 2)
|
||||
assert.Contains(t, org.Users, "existing-user@example.com")
|
||||
assert.Contains(t, org.Users, "new-user@example.com")
|
||||
assert.Equal(t, "admin@example.com", org.ChangedBy)
|
||||
assert.Equal(t, event.When(), org.ChangedAt)
|
||||
}
|
||||
|
||||
func TestUserAddedToOrganization_UpdateOrganization_DuplicateUser(t *testing.T) {
|
||||
event := &UserAddedToOrganization{
|
||||
BaseEvent: eventsourced.BaseEvent{
|
||||
EventTime: eventsourced.EventTime{
|
||||
Time: time.Now(),
|
||||
},
|
||||
},
|
||||
UserId: "existing-user@example.com",
|
||||
Initiator: "admin@example.com",
|
||||
}
|
||||
|
||||
org := &Organization{
|
||||
BaseAggregate: eventsourced.BaseAggregateFromString("org-123"),
|
||||
Users: []string{"existing-user@example.com"},
|
||||
ChangedBy: "previous-admin@example.com",
|
||||
}
|
||||
originalChangedBy := org.ChangedBy
|
||||
originalChangedAt := org.ChangedAt
|
||||
|
||||
event.UpdateOrganization(org)
|
||||
|
||||
// User should not be added twice
|
||||
assert.Len(t, org.Users, 1)
|
||||
assert.Equal(t, "existing-user@example.com", org.Users[0])
|
||||
|
||||
// ChangedBy and ChangedAt should NOT be updated when user already exists (idempotent)
|
||||
assert.Equal(t, originalChangedBy, org.ChangedBy)
|
||||
assert.Equal(t, originalChangedAt, org.ChangedAt)
|
||||
}
|
||||
|
||||
func TestAPIKeyRemoved_UpdateOrganization(t *testing.T) {
|
||||
event := &APIKeyRemoved{
|
||||
BaseEvent: eventsourced.BaseEvent{
|
||||
EventTime: eventsourced.EventTime{
|
||||
Time: time.Now(),
|
||||
},
|
||||
},
|
||||
KeyName: "production-key",
|
||||
Initiator: "admin@example.com",
|
||||
}
|
||||
|
||||
org := &Organization{
|
||||
BaseAggregate: eventsourced.BaseAggregateFromString("org-123"),
|
||||
APIKeys: []APIKey{
|
||||
{Name: "dev-key", Key: "hashed-key-1"},
|
||||
{Name: "production-key", Key: "hashed-key-2"},
|
||||
{Name: "staging-key", Key: "hashed-key-3"},
|
||||
},
|
||||
}
|
||||
|
||||
event.UpdateOrganization(org)
|
||||
|
||||
assert.Len(t, org.APIKeys, 2)
|
||||
assert.Equal(t, "dev-key", org.APIKeys[0].Name)
|
||||
assert.Equal(t, "staging-key", org.APIKeys[1].Name)
|
||||
assert.Equal(t, "admin@example.com", org.ChangedBy)
|
||||
assert.Equal(t, event.When(), org.ChangedAt)
|
||||
}
|
||||
|
||||
func TestAPIKeyRemoved_UpdateOrganization_KeyNotFound(t *testing.T) {
|
||||
event := &APIKeyRemoved{
|
||||
BaseEvent: eventsourced.BaseEvent{
|
||||
EventTime: eventsourced.EventTime{
|
||||
Time: time.Now(),
|
||||
},
|
||||
},
|
||||
KeyName: "non-existent-key",
|
||||
Initiator: "admin@example.com",
|
||||
}
|
||||
|
||||
org := &Organization{
|
||||
BaseAggregate: eventsourced.BaseAggregateFromString("org-123"),
|
||||
APIKeys: []APIKey{
|
||||
{Name: "dev-key", Key: "hashed-key-1"},
|
||||
{Name: "production-key", Key: "hashed-key-2"},
|
||||
},
|
||||
}
|
||||
|
||||
event.UpdateOrganization(org)
|
||||
|
||||
// No keys should be removed
|
||||
assert.Len(t, org.APIKeys, 2)
|
||||
assert.Equal(t, "dev-key", org.APIKeys[0].Name)
|
||||
assert.Equal(t, "production-key", org.APIKeys[1].Name)
|
||||
|
||||
// But metadata should still be updated
|
||||
assert.Equal(t, "admin@example.com", org.ChangedBy)
|
||||
assert.Equal(t, event.When(), org.ChangedAt)
|
||||
}
|
||||
|
||||
func TestAPIKeyRemoved_UpdateOrganization_OnlyKey(t *testing.T) {
|
||||
event := &APIKeyRemoved{
|
||||
BaseEvent: eventsourced.BaseEvent{
|
||||
EventTime: eventsourced.EventTime{
|
||||
Time: time.Now(),
|
||||
},
|
||||
},
|
||||
KeyName: "only-key",
|
||||
Initiator: "admin@example.com",
|
||||
}
|
||||
|
||||
org := &Organization{
|
||||
BaseAggregate: eventsourced.BaseAggregateFromString("org-123"),
|
||||
APIKeys: []APIKey{
|
||||
{Name: "only-key", Key: "hashed-key"},
|
||||
},
|
||||
}
|
||||
|
||||
event.UpdateOrganization(org)
|
||||
|
||||
// All keys should be removed
|
||||
assert.Len(t, org.APIKeys, 0)
|
||||
assert.Equal(t, "admin@example.com", org.ChangedBy)
|
||||
assert.Equal(t, event.When(), org.ChangedAt)
|
||||
}
|
||||
|
||||
func TestOrganizationRemoved_UpdateOrganization(t *testing.T) {
|
||||
event := &OrganizationRemoved{
|
||||
BaseEvent: eventsourced.BaseEvent{
|
||||
EventTime: eventsourced.EventTime{
|
||||
Time: time.Now(),
|
||||
},
|
||||
},
|
||||
Initiator: "admin@example.com",
|
||||
}
|
||||
|
||||
org := &Organization{
|
||||
BaseAggregate: eventsourced.BaseAggregateFromString("org-123"),
|
||||
Name: "Test Organization",
|
||||
Users: []string{"user1@example.com", "user2@example.com"},
|
||||
APIKeys: []APIKey{
|
||||
{Name: "key1", Key: "hashed-key-1"},
|
||||
},
|
||||
CreatedBy: "creator@example.com",
|
||||
CreatedAt: time.Now().Add(-24 * time.Hour),
|
||||
}
|
||||
|
||||
event.UpdateOrganization(org)
|
||||
|
||||
// Organization data remains (soft delete), but metadata is updated
|
||||
assert.Equal(t, "Test Organization", org.Name)
|
||||
assert.Len(t, org.Users, 2)
|
||||
assert.Len(t, org.APIKeys, 1)
|
||||
|
||||
// Metadata should be updated to reflect removal
|
||||
assert.Equal(t, "admin@example.com", org.ChangedBy)
|
||||
assert.Equal(t, event.When(), org.ChangedAt)
|
||||
}
|
||||
|
||||
func TestAPIKeyAdded_EnrichFromAggregate(t *testing.T) {
|
||||
orgId := "org-123"
|
||||
aggregate := &Organization{
|
||||
BaseAggregate: eventsourced.BaseAggregateFromString(orgId),
|
||||
}
|
||||
|
||||
event := &APIKeyAdded{
|
||||
Name: "test-key",
|
||||
Key: "hashed-key",
|
||||
Refs: []string{"main"},
|
||||
Read: true,
|
||||
Publish: false,
|
||||
Initiator: "user@example.com",
|
||||
}
|
||||
|
||||
event.EnrichFromAggregate(aggregate)
|
||||
|
||||
assert.Equal(t, orgId, event.OrganizationId)
|
||||
}
|
||||
|
||||
func TestSubGraphUpdated_Event(t *testing.T) {
|
||||
// Verify SubGraphUpdated event structure
|
||||
url := "http://service.example.com"
|
||||
wsUrl := "ws://service.example.com"
|
||||
|
||||
event := &SubGraphUpdated{
|
||||
OrganizationId: "org-123",
|
||||
Ref: "main",
|
||||
Service: "users-service",
|
||||
Url: &url,
|
||||
WSUrl: &wsUrl,
|
||||
Sdl: "type Query { user: User }",
|
||||
Initiator: "system",
|
||||
}
|
||||
|
||||
require.NotNil(t, event)
|
||||
assert.Equal(t, "org-123", event.OrganizationId)
|
||||
assert.Equal(t, "main", event.Ref)
|
||||
assert.Equal(t, "users-service", event.Service)
|
||||
assert.Equal(t, url, *event.Url)
|
||||
assert.Equal(t, wsUrl, *event.WSUrl)
|
||||
assert.Equal(t, "type Query { user: User }", event.Sdl)
|
||||
assert.Equal(t, "system", event.Initiator)
|
||||
}
|
||||
+411
-10
@@ -61,9 +61,12 @@ type ComplexityRoot struct {
|
||||
}
|
||||
|
||||
Mutation struct {
|
||||
AddAPIKey func(childComplexity int, input *model.InputAPIKey) int
|
||||
AddOrganization func(childComplexity int, name string) int
|
||||
UpdateSubGraph func(childComplexity int, input model.InputSubGraph) int
|
||||
AddAPIKey func(childComplexity int, input *model.InputAPIKey) int
|
||||
AddOrganization func(childComplexity int, name string) int
|
||||
AddUserToOrganization func(childComplexity int, organizationID string, userID string) int
|
||||
RemoveAPIKey func(childComplexity int, organizationID string, keyName string) int
|
||||
RemoveOrganization func(childComplexity int, organizationID string) int
|
||||
UpdateSubGraph func(childComplexity int, input model.InputSubGraph) int
|
||||
}
|
||||
|
||||
Organization struct {
|
||||
@@ -74,9 +77,10 @@ type ComplexityRoot struct {
|
||||
}
|
||||
|
||||
Query struct {
|
||||
LatestSchema func(childComplexity int, ref string) int
|
||||
Organizations func(childComplexity int) int
|
||||
Supergraph func(childComplexity int, ref string, isAfter *string) int
|
||||
AllOrganizations func(childComplexity int) int
|
||||
LatestSchema func(childComplexity int, ref string) int
|
||||
Organizations func(childComplexity int) int
|
||||
Supergraph func(childComplexity int, ref string, isAfter *string) int
|
||||
}
|
||||
|
||||
SchemaUpdate struct {
|
||||
@@ -119,11 +123,15 @@ type ComplexityRoot struct {
|
||||
|
||||
type MutationResolver interface {
|
||||
AddOrganization(ctx context.Context, name string) (*model.Organization, error)
|
||||
AddUserToOrganization(ctx context.Context, organizationID string, userID string) (*model.Organization, error)
|
||||
AddAPIKey(ctx context.Context, input *model.InputAPIKey) (*model.APIKey, error)
|
||||
RemoveAPIKey(ctx context.Context, organizationID string, keyName string) (*model.Organization, error)
|
||||
RemoveOrganization(ctx context.Context, organizationID string) (bool, error)
|
||||
UpdateSubGraph(ctx context.Context, input model.InputSubGraph) (*model.SubGraph, error)
|
||||
}
|
||||
type QueryResolver interface {
|
||||
Organizations(ctx context.Context) ([]*model.Organization, error)
|
||||
AllOrganizations(ctx context.Context) ([]*model.Organization, error)
|
||||
Supergraph(ctx context.Context, ref string, isAfter *string) (model.Supergraph, error)
|
||||
LatestSchema(ctx context.Context, ref string) (*model.SchemaUpdate, error)
|
||||
}
|
||||
@@ -215,6 +223,39 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
|
||||
}
|
||||
|
||||
return e.complexity.Mutation.AddOrganization(childComplexity, args["name"].(string)), true
|
||||
case "Mutation.addUserToOrganization":
|
||||
if e.complexity.Mutation.AddUserToOrganization == nil {
|
||||
break
|
||||
}
|
||||
|
||||
args, err := ec.field_Mutation_addUserToOrganization_args(ctx, rawArgs)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return e.complexity.Mutation.AddUserToOrganization(childComplexity, args["organizationId"].(string), args["userId"].(string)), true
|
||||
case "Mutation.removeAPIKey":
|
||||
if e.complexity.Mutation.RemoveAPIKey == nil {
|
||||
break
|
||||
}
|
||||
|
||||
args, err := ec.field_Mutation_removeAPIKey_args(ctx, rawArgs)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return e.complexity.Mutation.RemoveAPIKey(childComplexity, args["organizationId"].(string), args["keyName"].(string)), true
|
||||
case "Mutation.removeOrganization":
|
||||
if e.complexity.Mutation.RemoveOrganization == nil {
|
||||
break
|
||||
}
|
||||
|
||||
args, err := ec.field_Mutation_removeOrganization_args(ctx, rawArgs)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return e.complexity.Mutation.RemoveOrganization(childComplexity, args["organizationId"].(string)), true
|
||||
case "Mutation.updateSubGraph":
|
||||
if e.complexity.Mutation.UpdateSubGraph == nil {
|
||||
break
|
||||
@@ -252,6 +293,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
|
||||
|
||||
return e.complexity.Organization.Users(childComplexity), true
|
||||
|
||||
case "Query.allOrganizations":
|
||||
if e.complexity.Query.AllOrganizations == nil {
|
||||
break
|
||||
}
|
||||
|
||||
return e.complexity.Query.AllOrganizations(childComplexity), true
|
||||
case "Query.latestSchema":
|
||||
if e.complexity.Query.LatestSchema == nil {
|
||||
break
|
||||
@@ -532,13 +579,17 @@ func (ec *executionContext) introspectType(name string) (*introspection.Type, er
|
||||
var sources = []*ast.Source{
|
||||
{Name: "../schema.graphqls", Input: `type Query {
|
||||
organizations: [Organization!]! @auth(user: true)
|
||||
supergraph(ref: String!, isAfter: String): Supergraph! @auth(organization: true)
|
||||
latestSchema(ref: String!): SchemaUpdate! @auth(organization: true)
|
||||
allOrganizations: [Organization!]! @auth(user: true)
|
||||
supergraph(ref: String!, isAfter: String): Supergraph! @auth(user: true, organization: true)
|
||||
latestSchema(ref: String!): SchemaUpdate! @auth(user: true, organization: true)
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
addOrganization(name: String!): Organization! @auth(user: true)
|
||||
addUserToOrganization(organizationId: ID!, userId: String!): Organization! @auth(user: true)
|
||||
addAPIKey(input: InputAPIKey): APIKey! @auth(user: true)
|
||||
removeAPIKey(organizationId: ID!, keyName: String!): Organization! @auth(user: true)
|
||||
removeOrganization(organizationId: ID!): Boolean! @auth(user: true)
|
||||
updateSubGraph(input: InputSubGraph!): SubGraph! @auth(organization: true)
|
||||
}
|
||||
|
||||
@@ -663,6 +714,49 @@ func (ec *executionContext) field_Mutation_addOrganization_args(ctx context.Cont
|
||||
return args, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) field_Mutation_addUserToOrganization_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
|
||||
var err error
|
||||
args := map[string]any{}
|
||||
arg0, err := graphql.ProcessArgField(ctx, rawArgs, "organizationId", ec.unmarshalNID2string)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
args["organizationId"] = arg0
|
||||
arg1, err := graphql.ProcessArgField(ctx, rawArgs, "userId", ec.unmarshalNString2string)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
args["userId"] = arg1
|
||||
return args, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) field_Mutation_removeAPIKey_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
|
||||
var err error
|
||||
args := map[string]any{}
|
||||
arg0, err := graphql.ProcessArgField(ctx, rawArgs, "organizationId", ec.unmarshalNID2string)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
args["organizationId"] = arg0
|
||||
arg1, err := graphql.ProcessArgField(ctx, rawArgs, "keyName", ec.unmarshalNString2string)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
args["keyName"] = arg1
|
||||
return args, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) field_Mutation_removeOrganization_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
|
||||
var err error
|
||||
args := map[string]any{}
|
||||
arg0, err := graphql.ProcessArgField(ctx, rawArgs, "organizationId", ec.unmarshalNID2string)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
args["organizationId"] = arg0
|
||||
return args, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) field_Mutation_updateSubGraph_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
|
||||
var err error
|
||||
args := map[string]any{}
|
||||
@@ -1057,6 +1151,75 @@ func (ec *executionContext) fieldContext_Mutation_addOrganization(ctx context.Co
|
||||
return fc, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) _Mutation_addUserToOrganization(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||
return graphql.ResolveField(
|
||||
ctx,
|
||||
ec.OperationContext,
|
||||
field,
|
||||
ec.fieldContext_Mutation_addUserToOrganization,
|
||||
func(ctx context.Context) (any, error) {
|
||||
fc := graphql.GetFieldContext(ctx)
|
||||
return ec.resolvers.Mutation().AddUserToOrganization(ctx, fc.Args["organizationId"].(string), fc.Args["userId"].(string))
|
||||
},
|
||||
func(ctx context.Context, next graphql.Resolver) graphql.Resolver {
|
||||
directive0 := next
|
||||
|
||||
directive1 := func(ctx context.Context) (any, error) {
|
||||
user, err := ec.unmarshalOBoolean2ᚖbool(ctx, true)
|
||||
if err != nil {
|
||||
var zeroVal *model.Organization
|
||||
return zeroVal, err
|
||||
}
|
||||
if ec.directives.Auth == nil {
|
||||
var zeroVal *model.Organization
|
||||
return zeroVal, errors.New("directive auth is not implemented")
|
||||
}
|
||||
return ec.directives.Auth(ctx, nil, directive0, user, nil)
|
||||
}
|
||||
|
||||
next = directive1
|
||||
return next
|
||||
},
|
||||
ec.marshalNOrganization2ᚖgitlabᚗcomᚋunboundsoftwareᚋschemasᚋgraphᚋmodelᚐOrganization,
|
||||
true,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
func (ec *executionContext) fieldContext_Mutation_addUserToOrganization(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||
fc = &graphql.FieldContext{
|
||||
Object: "Mutation",
|
||||
Field: field,
|
||||
IsMethod: true,
|
||||
IsResolver: true,
|
||||
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||
switch field.Name {
|
||||
case "id":
|
||||
return ec.fieldContext_Organization_id(ctx, field)
|
||||
case "name":
|
||||
return ec.fieldContext_Organization_name(ctx, field)
|
||||
case "users":
|
||||
return ec.fieldContext_Organization_users(ctx, field)
|
||||
case "apiKeys":
|
||||
return ec.fieldContext_Organization_apiKeys(ctx, field)
|
||||
}
|
||||
return nil, fmt.Errorf("no field named %q was found under type Organization", field.Name)
|
||||
},
|
||||
}
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = ec.Recover(ctx, r)
|
||||
ec.Error(ctx, err)
|
||||
}
|
||||
}()
|
||||
ctx = graphql.WithFieldContext(ctx, fc)
|
||||
if fc.Args, err = ec.field_Mutation_addUserToOrganization_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
|
||||
ec.Error(ctx, err)
|
||||
return fc, err
|
||||
}
|
||||
return fc, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) _Mutation_addAPIKey(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||
return graphql.ResolveField(
|
||||
ctx,
|
||||
@@ -1132,6 +1295,134 @@ func (ec *executionContext) fieldContext_Mutation_addAPIKey(ctx context.Context,
|
||||
return fc, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) _Mutation_removeAPIKey(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||
return graphql.ResolveField(
|
||||
ctx,
|
||||
ec.OperationContext,
|
||||
field,
|
||||
ec.fieldContext_Mutation_removeAPIKey,
|
||||
func(ctx context.Context) (any, error) {
|
||||
fc := graphql.GetFieldContext(ctx)
|
||||
return ec.resolvers.Mutation().RemoveAPIKey(ctx, fc.Args["organizationId"].(string), fc.Args["keyName"].(string))
|
||||
},
|
||||
func(ctx context.Context, next graphql.Resolver) graphql.Resolver {
|
||||
directive0 := next
|
||||
|
||||
directive1 := func(ctx context.Context) (any, error) {
|
||||
user, err := ec.unmarshalOBoolean2ᚖbool(ctx, true)
|
||||
if err != nil {
|
||||
var zeroVal *model.Organization
|
||||
return zeroVal, err
|
||||
}
|
||||
if ec.directives.Auth == nil {
|
||||
var zeroVal *model.Organization
|
||||
return zeroVal, errors.New("directive auth is not implemented")
|
||||
}
|
||||
return ec.directives.Auth(ctx, nil, directive0, user, nil)
|
||||
}
|
||||
|
||||
next = directive1
|
||||
return next
|
||||
},
|
||||
ec.marshalNOrganization2ᚖgitlabᚗcomᚋunboundsoftwareᚋschemasᚋgraphᚋmodelᚐOrganization,
|
||||
true,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
func (ec *executionContext) fieldContext_Mutation_removeAPIKey(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||
fc = &graphql.FieldContext{
|
||||
Object: "Mutation",
|
||||
Field: field,
|
||||
IsMethod: true,
|
||||
IsResolver: true,
|
||||
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||
switch field.Name {
|
||||
case "id":
|
||||
return ec.fieldContext_Organization_id(ctx, field)
|
||||
case "name":
|
||||
return ec.fieldContext_Organization_name(ctx, field)
|
||||
case "users":
|
||||
return ec.fieldContext_Organization_users(ctx, field)
|
||||
case "apiKeys":
|
||||
return ec.fieldContext_Organization_apiKeys(ctx, field)
|
||||
}
|
||||
return nil, fmt.Errorf("no field named %q was found under type Organization", field.Name)
|
||||
},
|
||||
}
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = ec.Recover(ctx, r)
|
||||
ec.Error(ctx, err)
|
||||
}
|
||||
}()
|
||||
ctx = graphql.WithFieldContext(ctx, fc)
|
||||
if fc.Args, err = ec.field_Mutation_removeAPIKey_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
|
||||
ec.Error(ctx, err)
|
||||
return fc, err
|
||||
}
|
||||
return fc, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) _Mutation_removeOrganization(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||
return graphql.ResolveField(
|
||||
ctx,
|
||||
ec.OperationContext,
|
||||
field,
|
||||
ec.fieldContext_Mutation_removeOrganization,
|
||||
func(ctx context.Context) (any, error) {
|
||||
fc := graphql.GetFieldContext(ctx)
|
||||
return ec.resolvers.Mutation().RemoveOrganization(ctx, fc.Args["organizationId"].(string))
|
||||
},
|
||||
func(ctx context.Context, next graphql.Resolver) graphql.Resolver {
|
||||
directive0 := next
|
||||
|
||||
directive1 := func(ctx context.Context) (any, error) {
|
||||
user, err := ec.unmarshalOBoolean2ᚖbool(ctx, true)
|
||||
if err != nil {
|
||||
var zeroVal bool
|
||||
return zeroVal, err
|
||||
}
|
||||
if ec.directives.Auth == nil {
|
||||
var zeroVal bool
|
||||
return zeroVal, errors.New("directive auth is not implemented")
|
||||
}
|
||||
return ec.directives.Auth(ctx, nil, directive0, user, nil)
|
||||
}
|
||||
|
||||
next = directive1
|
||||
return next
|
||||
},
|
||||
ec.marshalNBoolean2bool,
|
||||
true,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
func (ec *executionContext) fieldContext_Mutation_removeOrganization(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||
fc = &graphql.FieldContext{
|
||||
Object: "Mutation",
|
||||
Field: field,
|
||||
IsMethod: true,
|
||||
IsResolver: true,
|
||||
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||
return nil, errors.New("field of type Boolean does not have child fields")
|
||||
},
|
||||
}
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = ec.Recover(ctx, r)
|
||||
ec.Error(ctx, err)
|
||||
}
|
||||
}()
|
||||
ctx = graphql.WithFieldContext(ctx, fc)
|
||||
if fc.Args, err = ec.field_Mutation_removeOrganization_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
|
||||
ec.Error(ctx, err)
|
||||
return fc, err
|
||||
}
|
||||
return fc, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) _Mutation_updateSubGraph(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||
return graphql.ResolveField(
|
||||
ctx,
|
||||
@@ -1400,6 +1691,63 @@ func (ec *executionContext) fieldContext_Query_organizations(_ context.Context,
|
||||
return fc, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) _Query_allOrganizations(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||
return graphql.ResolveField(
|
||||
ctx,
|
||||
ec.OperationContext,
|
||||
field,
|
||||
ec.fieldContext_Query_allOrganizations,
|
||||
func(ctx context.Context) (any, error) {
|
||||
return ec.resolvers.Query().AllOrganizations(ctx)
|
||||
},
|
||||
func(ctx context.Context, next graphql.Resolver) graphql.Resolver {
|
||||
directive0 := next
|
||||
|
||||
directive1 := func(ctx context.Context) (any, error) {
|
||||
user, err := ec.unmarshalOBoolean2ᚖbool(ctx, true)
|
||||
if err != nil {
|
||||
var zeroVal []*model.Organization
|
||||
return zeroVal, err
|
||||
}
|
||||
if ec.directives.Auth == nil {
|
||||
var zeroVal []*model.Organization
|
||||
return zeroVal, errors.New("directive auth is not implemented")
|
||||
}
|
||||
return ec.directives.Auth(ctx, nil, directive0, user, nil)
|
||||
}
|
||||
|
||||
next = directive1
|
||||
return next
|
||||
},
|
||||
ec.marshalNOrganization2ᚕᚖgitlabᚗcomᚋunboundsoftwareᚋschemasᚋgraphᚋmodelᚐOrganizationᚄ,
|
||||
true,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
func (ec *executionContext) fieldContext_Query_allOrganizations(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||
fc = &graphql.FieldContext{
|
||||
Object: "Query",
|
||||
Field: field,
|
||||
IsMethod: true,
|
||||
IsResolver: true,
|
||||
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||
switch field.Name {
|
||||
case "id":
|
||||
return ec.fieldContext_Organization_id(ctx, field)
|
||||
case "name":
|
||||
return ec.fieldContext_Organization_name(ctx, field)
|
||||
case "users":
|
||||
return ec.fieldContext_Organization_users(ctx, field)
|
||||
case "apiKeys":
|
||||
return ec.fieldContext_Organization_apiKeys(ctx, field)
|
||||
}
|
||||
return nil, fmt.Errorf("no field named %q was found under type Organization", field.Name)
|
||||
},
|
||||
}
|
||||
return fc, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) _Query_supergraph(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||
return graphql.ResolveField(
|
||||
ctx,
|
||||
@@ -1414,6 +1762,11 @@ func (ec *executionContext) _Query_supergraph(ctx context.Context, field graphql
|
||||
directive0 := next
|
||||
|
||||
directive1 := func(ctx context.Context) (any, error) {
|
||||
user, err := ec.unmarshalOBoolean2ᚖbool(ctx, true)
|
||||
if err != nil {
|
||||
var zeroVal model.Supergraph
|
||||
return zeroVal, err
|
||||
}
|
||||
organization, err := ec.unmarshalOBoolean2ᚖbool(ctx, true)
|
||||
if err != nil {
|
||||
var zeroVal model.Supergraph
|
||||
@@ -1423,7 +1776,7 @@ func (ec *executionContext) _Query_supergraph(ctx context.Context, field graphql
|
||||
var zeroVal model.Supergraph
|
||||
return zeroVal, errors.New("directive auth is not implemented")
|
||||
}
|
||||
return ec.directives.Auth(ctx, nil, directive0, nil, organization)
|
||||
return ec.directives.Auth(ctx, nil, directive0, user, organization)
|
||||
}
|
||||
|
||||
next = directive1
|
||||
@@ -1473,6 +1826,11 @@ func (ec *executionContext) _Query_latestSchema(ctx context.Context, field graph
|
||||
directive0 := next
|
||||
|
||||
directive1 := func(ctx context.Context) (any, error) {
|
||||
user, err := ec.unmarshalOBoolean2ᚖbool(ctx, true)
|
||||
if err != nil {
|
||||
var zeroVal *model.SchemaUpdate
|
||||
return zeroVal, err
|
||||
}
|
||||
organization, err := ec.unmarshalOBoolean2ᚖbool(ctx, true)
|
||||
if err != nil {
|
||||
var zeroVal *model.SchemaUpdate
|
||||
@@ -1482,7 +1840,7 @@ func (ec *executionContext) _Query_latestSchema(ctx context.Context, field graph
|
||||
var zeroVal *model.SchemaUpdate
|
||||
return zeroVal, errors.New("directive auth is not implemented")
|
||||
}
|
||||
return ec.directives.Auth(ctx, nil, directive0, nil, organization)
|
||||
return ec.directives.Auth(ctx, nil, directive0, user, organization)
|
||||
}
|
||||
|
||||
next = directive1
|
||||
@@ -3938,6 +4296,13 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
|
||||
if out.Values[i] == graphql.Null {
|
||||
out.Invalids++
|
||||
}
|
||||
case "addUserToOrganization":
|
||||
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
|
||||
return ec._Mutation_addUserToOrganization(ctx, field)
|
||||
})
|
||||
if out.Values[i] == graphql.Null {
|
||||
out.Invalids++
|
||||
}
|
||||
case "addAPIKey":
|
||||
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
|
||||
return ec._Mutation_addAPIKey(ctx, field)
|
||||
@@ -3945,6 +4310,20 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
|
||||
if out.Values[i] == graphql.Null {
|
||||
out.Invalids++
|
||||
}
|
||||
case "removeAPIKey":
|
||||
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
|
||||
return ec._Mutation_removeAPIKey(ctx, field)
|
||||
})
|
||||
if out.Values[i] == graphql.Null {
|
||||
out.Invalids++
|
||||
}
|
||||
case "removeOrganization":
|
||||
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
|
||||
return ec._Mutation_removeOrganization(ctx, field)
|
||||
})
|
||||
if out.Values[i] == graphql.Null {
|
||||
out.Invalids++
|
||||
}
|
||||
case "updateSubGraph":
|
||||
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
|
||||
return ec._Mutation_updateSubGraph(ctx, field)
|
||||
@@ -4069,6 +4448,28 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
|
||||
func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
|
||||
}
|
||||
|
||||
out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) })
|
||||
case "allOrganizations":
|
||||
field := field
|
||||
|
||||
innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ec.Error(ctx, ec.Recover(ctx, r))
|
||||
}
|
||||
}()
|
||||
res = ec._Query_allOrganizations(ctx, field)
|
||||
if res == graphql.Null {
|
||||
atomic.AddUint32(&fs.Invalids, 1)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
rrm := func(ctx context.Context) graphql.Marshaler {
|
||||
return ec.OperationContext.RootResolverMiddleware(ctx,
|
||||
func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
|
||||
}
|
||||
|
||||
out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) })
|
||||
case "supergraph":
|
||||
field := field
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
type Query {
|
||||
organizations: [Organization!]! @auth(user: true)
|
||||
supergraph(ref: String!, isAfter: String): Supergraph! @auth(organization: true)
|
||||
latestSchema(ref: String!): SchemaUpdate! @auth(organization: true)
|
||||
allOrganizations: [Organization!]! @auth(user: true)
|
||||
supergraph(ref: String!, isAfter: String): Supergraph! @auth(user: true, organization: true)
|
||||
latestSchema(ref: String!): SchemaUpdate! @auth(user: true, organization: true)
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
addOrganization(name: String!): Organization! @auth(user: true)
|
||||
addUserToOrganization(organizationId: ID!, userId: String!): Organization! @auth(user: true)
|
||||
addAPIKey(input: InputAPIKey): APIKey! @auth(user: true)
|
||||
removeAPIKey(organizationId: ID!, keyName: String!): Organization! @auth(user: true)
|
||||
removeOrganization(organizationId: ID!): Boolean! @auth(user: true)
|
||||
updateSubGraph(input: InputSubGraph!): SubGraph! @auth(organization: true)
|
||||
}
|
||||
|
||||
|
||||
+114
-7
@@ -37,6 +37,24 @@ func (r *mutationResolver) AddOrganization(ctx context.Context, name string) (*m
|
||||
return ToGqlOrganization(*org), nil
|
||||
}
|
||||
|
||||
// AddUserToOrganization is the resolver for the addUserToOrganization field.
|
||||
func (r *mutationResolver) AddUserToOrganization(ctx context.Context, organizationID string, userID string) (*model.Organization, error) {
|
||||
sub := middleware.UserFromContext(ctx)
|
||||
org := &domain.Organization{BaseAggregate: eventsourced.BaseAggregateFromString(organizationID)}
|
||||
h, err := r.handler(ctx, org)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = h.Handle(ctx, &domain.AddUserToOrganization{
|
||||
UserId: userID,
|
||||
Initiator: sub,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ToGqlOrganization(*org), nil
|
||||
}
|
||||
|
||||
// AddAPIKey is the resolver for the addAPIKey field.
|
||||
func (r *mutationResolver) AddAPIKey(ctx context.Context, input *model.InputAPIKey) (*model.APIKey, error) {
|
||||
sub := middleware.UserFromContext(ctx)
|
||||
@@ -71,6 +89,41 @@ func (r *mutationResolver) AddAPIKey(ctx context.Context, input *model.InputAPIK
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RemoveAPIKey is the resolver for the removeAPIKey field.
|
||||
func (r *mutationResolver) RemoveAPIKey(ctx context.Context, organizationID string, keyName string) (*model.Organization, error) {
|
||||
sub := middleware.UserFromContext(ctx)
|
||||
org := &domain.Organization{BaseAggregate: eventsourced.BaseAggregateFromString(organizationID)}
|
||||
h, err := r.handler(ctx, org)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = h.Handle(ctx, &domain.RemoveAPIKey{
|
||||
KeyName: keyName,
|
||||
Initiator: sub,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ToGqlOrganization(*org), nil
|
||||
}
|
||||
|
||||
// RemoveOrganization is the resolver for the removeOrganization field.
|
||||
func (r *mutationResolver) RemoveOrganization(ctx context.Context, organizationID string) (bool, error) {
|
||||
sub := middleware.UserFromContext(ctx)
|
||||
org := &domain.Organization{BaseAggregate: eventsourced.BaseAggregateFromString(organizationID)}
|
||||
h, err := r.handler(ctx, org)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
_, err = h.Handle(ctx, &domain.RemoveOrganization{
|
||||
Initiator: sub,
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// UpdateSubGraph is the resolver for the updateSubGraph field.
|
||||
func (r *mutationResolver) UpdateSubGraph(ctx context.Context, input model.InputSubGraph) (*model.SubGraph, error) {
|
||||
orgId := middleware.OrganizationFromContext(ctx)
|
||||
@@ -183,13 +236,49 @@ func (r *queryResolver) Organizations(ctx context.Context) ([]*model.Organizatio
|
||||
return ToGqlOrganizations(orgs), nil
|
||||
}
|
||||
|
||||
// AllOrganizations is the resolver for the allOrganizations field.
|
||||
func (r *queryResolver) AllOrganizations(ctx context.Context) ([]*model.Organization, error) {
|
||||
// Check if user has admin role
|
||||
if !middleware.UserHasRole(ctx, "admin") {
|
||||
return nil, fmt.Errorf("unauthorized: admin role required")
|
||||
}
|
||||
|
||||
orgs := r.Cache.AllOrganizations()
|
||||
return ToGqlOrganizations(orgs), nil
|
||||
}
|
||||
|
||||
// Supergraph is the resolver for the supergraph field.
|
||||
func (r *queryResolver) Supergraph(ctx context.Context, ref string, isAfter *string) (model.Supergraph, error) {
|
||||
orgId := middleware.OrganizationFromContext(ctx)
|
||||
_, err := r.apiKeyCanAccessRef(ctx, ref, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
userId := middleware.UserFromContext(ctx)
|
||||
|
||||
r.Logger.Info("Supergraph query",
|
||||
"ref", ref,
|
||||
"orgId", orgId,
|
||||
"userId", userId,
|
||||
)
|
||||
|
||||
// If authenticated with API key (organization), check access
|
||||
if orgId != "" {
|
||||
_, err := r.apiKeyCanAccessRef(ctx, ref, false)
|
||||
if err != nil {
|
||||
r.Logger.Error("API key cannot access ref", "error", err, "ref", ref)
|
||||
return nil, err
|
||||
}
|
||||
} else if userId != "" {
|
||||
// For user authentication, check if user has access to ref through their organizations
|
||||
userOrgs := r.Cache.OrganizationsByUser(userId)
|
||||
if len(userOrgs) == 0 {
|
||||
r.Logger.Error("User has no organizations", "userId", userId)
|
||||
return nil, fmt.Errorf("user has no access to any organizations")
|
||||
}
|
||||
// Use the first organization's ID for querying
|
||||
orgId = userOrgs[0].ID.String()
|
||||
r.Logger.Info("Using organization from user context", "orgId", orgId)
|
||||
} else {
|
||||
return nil, fmt.Errorf("no authentication provided")
|
||||
}
|
||||
|
||||
after := ""
|
||||
if isAfter != nil {
|
||||
after = *isAfter
|
||||
@@ -241,16 +330,34 @@ func (r *queryResolver) Supergraph(ctx context.Context, ref string, isAfter *str
|
||||
// LatestSchema is the resolver for the latestSchema field.
|
||||
func (r *queryResolver) LatestSchema(ctx context.Context, ref string) (*model.SchemaUpdate, error) {
|
||||
orgId := middleware.OrganizationFromContext(ctx)
|
||||
userId := middleware.UserFromContext(ctx)
|
||||
|
||||
r.Logger.Info("LatestSchema query",
|
||||
"ref", ref,
|
||||
"orgId", orgId,
|
||||
"userId", userId,
|
||||
)
|
||||
|
||||
_, err := r.apiKeyCanAccessRef(ctx, ref, false)
|
||||
if err != nil {
|
||||
r.Logger.Error("API key cannot access ref", "error", err, "ref", ref)
|
||||
return nil, err
|
||||
// If authenticated with API key (organization), check access
|
||||
if orgId != "" {
|
||||
_, err := r.apiKeyCanAccessRef(ctx, ref, false)
|
||||
if err != nil {
|
||||
r.Logger.Error("API key cannot access ref", "error", err, "ref", ref)
|
||||
return nil, err
|
||||
}
|
||||
} else if userId != "" {
|
||||
// For user authentication, check if user has access to ref through their organizations
|
||||
userOrgs := r.Cache.OrganizationsByUser(userId)
|
||||
if len(userOrgs) == 0 {
|
||||
r.Logger.Error("User has no organizations", "userId", userId)
|
||||
return nil, fmt.Errorf("user has no access to any organizations")
|
||||
}
|
||||
// Use the first organization's ID for querying
|
||||
// In a real-world scenario, you might want to check which org has access to this ref
|
||||
orgId = userOrgs[0].ID.String()
|
||||
r.Logger.Info("Using organization from user context", "orgId", orgId)
|
||||
} else {
|
||||
return nil, fmt.Errorf("no authentication provided")
|
||||
}
|
||||
|
||||
// Get current services and schema
|
||||
|
||||
+62
-4
@@ -67,6 +67,37 @@ func UserFromContext(ctx context.Context) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func UserHasRole(ctx context.Context, role string) bool {
|
||||
token, err := TokenFromContext(ctx)
|
||||
if err != nil || token == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check the custom roles claim
|
||||
rolesInterface, ok := claims["https://unbound.se/roles"]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
roles, ok := rolesInterface.([]interface{})
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, r := range roles {
|
||||
if roleStr, ok := r.(string); ok && roleStr == role {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func OrganizationFromContext(ctx context.Context) string {
|
||||
if value := ctx.Value(OrganizationKey); value != nil {
|
||||
if u, ok := value.(domain.Organization); ok {
|
||||
@@ -77,15 +108,42 @@ func OrganizationFromContext(ctx context.Context) string {
|
||||
}
|
||||
|
||||
func (m *AuthMiddleware) Directive(ctx context.Context, _ interface{}, next graphql.Resolver, user *bool, organization *bool) (res interface{}, err error) {
|
||||
if user != nil && *user {
|
||||
if u := UserFromContext(ctx); u == "" {
|
||||
userRequired := user != nil && *user
|
||||
orgRequired := organization != nil && *organization
|
||||
|
||||
u := UserFromContext(ctx)
|
||||
orgId := OrganizationFromContext(ctx)
|
||||
|
||||
fmt.Printf("[Auth Directive] userRequired=%v, orgRequired=%v, hasUser=%v, hasOrg=%v\n",
|
||||
userRequired, orgRequired, u != "", orgId != "")
|
||||
|
||||
// If both are required, it means EITHER is acceptable (OR logic)
|
||||
if userRequired && orgRequired {
|
||||
if u == "" && orgId == "" {
|
||||
fmt.Printf("[Auth Directive] REJECTED: Neither user nor organization available\n")
|
||||
return nil, fmt.Errorf("authentication required: provide either user token or organization API key")
|
||||
}
|
||||
fmt.Printf("[Auth Directive] ACCEPTED: Has user=%v OR organization=%v\n", u != "", orgId != "")
|
||||
return next(ctx)
|
||||
}
|
||||
|
||||
// Only user required
|
||||
if userRequired {
|
||||
if u == "" {
|
||||
fmt.Printf("[Auth Directive] REJECTED: No user available\n")
|
||||
return nil, fmt.Errorf("no user available in request")
|
||||
}
|
||||
fmt.Printf("[Auth Directive] ACCEPTED: User authenticated\n")
|
||||
}
|
||||
if organization != nil && *organization {
|
||||
if orgId := OrganizationFromContext(ctx); orgId == "" {
|
||||
|
||||
// Only organization required
|
||||
if orgRequired {
|
||||
if orgId == "" {
|
||||
fmt.Printf("[Auth Directive] REJECTED: No organization available\n")
|
||||
return nil, fmt.Errorf("no organization available in request")
|
||||
}
|
||||
fmt.Printf("[Auth Directive] ACCEPTED: Organization authenticated\n")
|
||||
}
|
||||
|
||||
return next(ctx)
|
||||
}
|
||||
|
||||
+104
-4
@@ -427,7 +427,10 @@ func TestAuthMiddleware_Directive_RequiresBoth(t *testing.T) {
|
||||
Name: "Test Org",
|
||||
}
|
||||
|
||||
// Test with both present
|
||||
// When both user and organization are marked as acceptable,
|
||||
// the directive uses OR logic - either one is sufficient
|
||||
|
||||
// Test with both present - should succeed
|
||||
ctx := context.WithValue(context.Background(), UserKey, "user-123")
|
||||
ctx = context.WithValue(ctx, OrganizationKey, org)
|
||||
_, err := authMiddleware.Directive(ctx, nil, func(ctx context.Context) (interface{}, error) {
|
||||
@@ -435,19 +438,27 @@ func TestAuthMiddleware_Directive_RequiresBoth(t *testing.T) {
|
||||
}, &requireUser, &requireOrg)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test with only user
|
||||
// Test with only user - should succeed (OR logic)
|
||||
ctx = context.WithValue(context.Background(), UserKey, "user-123")
|
||||
_, err = authMiddleware.Directive(ctx, nil, func(ctx context.Context) (interface{}, error) {
|
||||
return "success", nil
|
||||
}, &requireUser, &requireOrg)
|
||||
assert.Error(t, err)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test with only organization
|
||||
// Test with only organization - should succeed (OR logic)
|
||||
ctx = context.WithValue(context.Background(), OrganizationKey, org)
|
||||
_, err = authMiddleware.Directive(ctx, nil, func(ctx context.Context) (interface{}, error) {
|
||||
return "success", nil
|
||||
}, &requireUser, &requireOrg)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test with neither - should fail
|
||||
ctx = context.Background()
|
||||
_, err = authMiddleware.Directive(ctx, nil, func(ctx context.Context) (interface{}, error) {
|
||||
return "success", nil
|
||||
}, &requireUser, &requireOrg)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "authentication required")
|
||||
}
|
||||
|
||||
func TestAuthMiddleware_Directive_NoRequirements(t *testing.T) {
|
||||
@@ -462,3 +473,92 @@ func TestAuthMiddleware_Directive_NoRequirements(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "success", result)
|
||||
}
|
||||
|
||||
func TestUserHasRole_WithValidRole(t *testing.T) {
|
||||
// Create token with roles claim
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"sub": "user-123",
|
||||
"https://unbound.se/roles": []interface{}{"admin", "user"},
|
||||
})
|
||||
|
||||
ctx := context.WithValue(context.Background(), mw.ContextKey{}, token)
|
||||
|
||||
// Test for existing role
|
||||
hasRole := UserHasRole(ctx, "admin")
|
||||
assert.True(t, hasRole)
|
||||
|
||||
hasRole = UserHasRole(ctx, "user")
|
||||
assert.True(t, hasRole)
|
||||
}
|
||||
|
||||
func TestUserHasRole_WithoutRole(t *testing.T) {
|
||||
// Create token with roles claim
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"sub": "user-123",
|
||||
"https://unbound.se/roles": []interface{}{"user"},
|
||||
})
|
||||
|
||||
ctx := context.WithValue(context.Background(), mw.ContextKey{}, token)
|
||||
|
||||
// Test for non-existing role
|
||||
hasRole := UserHasRole(ctx, "admin")
|
||||
assert.False(t, hasRole)
|
||||
}
|
||||
|
||||
func TestUserHasRole_WithoutRolesClaim(t *testing.T) {
|
||||
// Create token without roles claim
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"sub": "user-123",
|
||||
})
|
||||
|
||||
ctx := context.WithValue(context.Background(), mw.ContextKey{}, token)
|
||||
|
||||
// Test should return false when roles claim is missing
|
||||
hasRole := UserHasRole(ctx, "admin")
|
||||
assert.False(t, hasRole)
|
||||
}
|
||||
|
||||
func TestUserHasRole_WithoutToken(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Test should return false when no token in context
|
||||
hasRole := UserHasRole(ctx, "admin")
|
||||
assert.False(t, hasRole)
|
||||
}
|
||||
|
||||
func TestUserHasRole_WithInvalidTokenType(t *testing.T) {
|
||||
// Put invalid token type in context
|
||||
ctx := context.WithValue(context.Background(), mw.ContextKey{}, "not-a-token")
|
||||
|
||||
// Test should return false when token type is invalid
|
||||
hasRole := UserHasRole(ctx, "admin")
|
||||
assert.False(t, hasRole)
|
||||
}
|
||||
|
||||
func TestUserHasRole_WithInvalidRolesType(t *testing.T) {
|
||||
// Create token with invalid roles type
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"sub": "user-123",
|
||||
"https://unbound.se/roles": "not-an-array",
|
||||
})
|
||||
|
||||
ctx := context.WithValue(context.Background(), mw.ContextKey{}, token)
|
||||
|
||||
// Test should return false when roles type is invalid
|
||||
hasRole := UserHasRole(ctx, "admin")
|
||||
assert.False(t, hasRole)
|
||||
}
|
||||
|
||||
func TestUserHasRole_WithInvalidRoleElementType(t *testing.T) {
|
||||
// Create token with invalid role element types
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"sub": "user-123",
|
||||
"https://unbound.se/roles": []interface{}{123, 456}, // Numbers instead of strings
|
||||
})
|
||||
|
||||
ctx := context.WithValue(context.Background(), mw.ContextKey{}, token)
|
||||
|
||||
// Test should return false when role elements are not strings
|
||||
hasRole := UserHasRole(ctx, "admin")
|
||||
assert.False(t, hasRole)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package sdlmerge
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@@ -61,12 +62,13 @@ func MergeSDLs(SDLs ...string) (string, error) {
|
||||
return "", fmt.Errorf("merge ast: %w", err)
|
||||
}
|
||||
|
||||
out, err := astprinter.PrintString(&doc)
|
||||
if err != nil {
|
||||
// Format with indentation for better readability
|
||||
buf := &bytes.Buffer{}
|
||||
if err := astprinter.PrintIndent(&doc, []byte(" "), buf); err != nil {
|
||||
return "", fmt.Errorf("stringify schema: %w", err)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func validateSubgraphs(subgraphs []string) error {
|
||||
|
||||
Reference in New Issue
Block a user