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
|
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 {
|
func (c *Cache) ApiKeyByKey(key string) *domain.APIKey {
|
||||||
c.mu.RLock()
|
c.mu.RLock()
|
||||||
defer c.mu.RUnlock()
|
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.organizations[m.ID.String()] = o
|
||||||
c.addUser(m.Initiator, o)
|
c.addUser(m.Initiator, o)
|
||||||
c.logger.With("org_id", m.ID.String(), "event", "OrganizationAdded").Debug("cache updated")
|
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:
|
case *domain.APIKeyAdded:
|
||||||
key := domain.APIKey{
|
key := domain.APIKey{
|
||||||
Name: m.Name,
|
Name: m.Name,
|
||||||
@@ -117,6 +138,63 @@ func (c *Cache) Update(msg any, _ goamqp.Headers) (any, error) {
|
|||||||
org.APIKeys = append(org.APIKeys, key)
|
org.APIKeys = append(org.APIKeys, key)
|
||||||
c.organizations[m.OrganizationId] = org
|
c.organizations[m.OrganizationId] = org
|
||||||
c.logger.With("org_id", m.OrganizationId, "key_name", m.Name, "event", "APIKeyAdded").Debug("cache updated")
|
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:
|
case *domain.SubGraphUpdated:
|
||||||
c.updateSubGraph(m.OrganizationId, m.Ref, m.ID.String(), m.Service, m.Time)
|
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")
|
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
|
// Verify cache is in consistent state
|
||||||
assert.GreaterOrEqual(t, len(c.organizations), numWriters)
|
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(
|
pg.WithEventTypes(
|
||||||
&domain.SubGraphUpdated{},
|
&domain.SubGraphUpdated{},
|
||||||
&domain.OrganizationAdded{},
|
&domain.OrganizationAdded{},
|
||||||
|
&domain.UserAddedToOrganization{},
|
||||||
&domain.APIKeyAdded{},
|
&domain.APIKeyAdded{},
|
||||||
|
&domain.APIKeyRemoved{},
|
||||||
|
&domain.OrganizationRemoved{},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -127,10 +130,16 @@ func start(closeEvents chan error, logger *slog.Logger, connectToAmqpFunc func(u
|
|||||||
goamqp.EventStreamPublisher(publisher),
|
goamqp.EventStreamPublisher(publisher),
|
||||||
goamqp.TransientEventStreamConsumer("SubGraph.Updated", serviceCache.Update, domain.SubGraphUpdated{}),
|
goamqp.TransientEventStreamConsumer("SubGraph.Updated", serviceCache.Update, domain.SubGraphUpdated{}),
|
||||||
goamqp.TransientEventStreamConsumer("Organization.Added", serviceCache.Update, domain.OrganizationAdded{}),
|
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.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("SubGraph.Updated", domain.SubGraphUpdated{}),
|
||||||
goamqp.WithTypeMapping("Organization.Added", domain.OrganizationAdded{}),
|
goamqp.WithTypeMapping("Organization.Added", domain.OrganizationAdded{}),
|
||||||
|
goamqp.WithTypeMapping("Organization.UserAdded", domain.UserAddedToOrganization{}),
|
||||||
goamqp.WithTypeMapping("Organization.APIKeyAdded", domain.APIKeyAdded{}),
|
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 {
|
if err := conn.Start(rootCtx, setups...); err != nil {
|
||||||
return fmt.Errorf("failed to setup AMQP: %v", err)
|
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) {
|
switch e := event.(type) {
|
||||||
case *OrganizationAdded:
|
case *OrganizationAdded:
|
||||||
e.UpdateOrganization(o)
|
e.UpdateOrganization(o)
|
||||||
|
case *UserAddedToOrganization:
|
||||||
|
e.UpdateOrganization(o)
|
||||||
case *APIKeyAdded:
|
case *APIKeyAdded:
|
||||||
o.APIKeys = append(o.APIKeys, APIKey{
|
o.APIKeys = append(o.APIKeys, APIKey{
|
||||||
Name: e.Name,
|
Name: e.Name,
|
||||||
@@ -36,6 +38,10 @@ func (o *Organization) Apply(event eventsourced.Event) error {
|
|||||||
})
|
})
|
||||||
o.ChangedBy = e.Initiator
|
o.ChangedBy = e.Initiator
|
||||||
o.ChangedAt = e.When()
|
o.ChangedAt = e.When()
|
||||||
|
case *APIKeyRemoved:
|
||||||
|
e.UpdateOrganization(o)
|
||||||
|
case *OrganizationRemoved:
|
||||||
|
e.UpdateOrganization(o)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unexpected event type: %+v", event)
|
return fmt.Errorf("unexpected event type: %+v", event)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,37 @@ func (a AddOrganization) Event(context.Context) eventsourced.Event {
|
|||||||
|
|
||||||
var _ eventsourced.Command = AddOrganization{}
|
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 {
|
type AddAPIKey struct {
|
||||||
Name string
|
Name string
|
||||||
Key string
|
Key string
|
||||||
@@ -79,6 +110,57 @@ func (a AddAPIKey) Event(context.Context) eventsourced.Event {
|
|||||||
|
|
||||||
var _ eventsourced.Command = AddAPIKey{}
|
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 {
|
type UpdateSubGraph struct {
|
||||||
OrganizationId string
|
OrganizationId string
|
||||||
Ref 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, "type Query { hello: String }", subGraphEvent.Sdl)
|
||||||
assert.Equal(t, "user@example.com", subGraphEvent.Initiator)
|
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()
|
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 {
|
type APIKeyAdded struct {
|
||||||
eventsourced.BaseEvent
|
eventsourced.BaseEvent
|
||||||
OrganizationId string `json:"organizationId"`
|
OrganizationId string `json:"organizationId"`
|
||||||
@@ -34,6 +52,36 @@ func (a *APIKeyAdded) EnrichFromAggregate(aggregate eventsourced.Aggregate) {
|
|||||||
|
|
||||||
var _ eventsourced.EnrichableEvent = &APIKeyAdded{}
|
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 {
|
type SubGraphUpdated struct {
|
||||||
eventsourced.BaseEvent
|
eventsourced.BaseEvent
|
||||||
OrganizationId string `json:"organizationId"`
|
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)
|
||||||
|
}
|
||||||
@@ -63,6 +63,9 @@ type ComplexityRoot struct {
|
|||||||
Mutation struct {
|
Mutation struct {
|
||||||
AddAPIKey func(childComplexity int, input *model.InputAPIKey) int
|
AddAPIKey func(childComplexity int, input *model.InputAPIKey) int
|
||||||
AddOrganization func(childComplexity int, name string) 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
|
UpdateSubGraph func(childComplexity int, input model.InputSubGraph) int
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,6 +77,7 @@ type ComplexityRoot struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Query struct {
|
Query struct {
|
||||||
|
AllOrganizations func(childComplexity int) int
|
||||||
LatestSchema func(childComplexity int, ref string) int
|
LatestSchema func(childComplexity int, ref string) int
|
||||||
Organizations func(childComplexity int) int
|
Organizations func(childComplexity int) int
|
||||||
Supergraph func(childComplexity int, ref string, isAfter *string) int
|
Supergraph func(childComplexity int, ref string, isAfter *string) int
|
||||||
@@ -119,11 +123,15 @@ type ComplexityRoot struct {
|
|||||||
|
|
||||||
type MutationResolver interface {
|
type MutationResolver interface {
|
||||||
AddOrganization(ctx context.Context, name string) (*model.Organization, error)
|
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)
|
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)
|
UpdateSubGraph(ctx context.Context, input model.InputSubGraph) (*model.SubGraph, error)
|
||||||
}
|
}
|
||||||
type QueryResolver interface {
|
type QueryResolver interface {
|
||||||
Organizations(ctx context.Context) ([]*model.Organization, error)
|
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)
|
Supergraph(ctx context.Context, ref string, isAfter *string) (model.Supergraph, error)
|
||||||
LatestSchema(ctx context.Context, ref string) (*model.SchemaUpdate, 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
|
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":
|
case "Mutation.updateSubGraph":
|
||||||
if e.complexity.Mutation.UpdateSubGraph == nil {
|
if e.complexity.Mutation.UpdateSubGraph == nil {
|
||||||
break
|
break
|
||||||
@@ -252,6 +293,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
|
|||||||
|
|
||||||
return e.complexity.Organization.Users(childComplexity), true
|
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":
|
case "Query.latestSchema":
|
||||||
if e.complexity.Query.LatestSchema == nil {
|
if e.complexity.Query.LatestSchema == nil {
|
||||||
break
|
break
|
||||||
@@ -532,13 +579,17 @@ func (ec *executionContext) introspectType(name string) (*introspection.Type, er
|
|||||||
var sources = []*ast.Source{
|
var sources = []*ast.Source{
|
||||||
{Name: "../schema.graphqls", Input: `type Query {
|
{Name: "../schema.graphqls", Input: `type Query {
|
||||||
organizations: [Organization!]! @auth(user: true)
|
organizations: [Organization!]! @auth(user: true)
|
||||||
supergraph(ref: String!, isAfter: String): Supergraph! @auth(organization: true)
|
allOrganizations: [Organization!]! @auth(user: true)
|
||||||
latestSchema(ref: String!): SchemaUpdate! @auth(organization: true)
|
supergraph(ref: String!, isAfter: String): Supergraph! @auth(user: true, organization: true)
|
||||||
|
latestSchema(ref: String!): SchemaUpdate! @auth(user: true, organization: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
addOrganization(name: String!): Organization! @auth(user: true)
|
addOrganization(name: String!): Organization! @auth(user: true)
|
||||||
|
addUserToOrganization(organizationId: ID!, userId: String!): Organization! @auth(user: true)
|
||||||
addAPIKey(input: InputAPIKey): APIKey! @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)
|
updateSubGraph(input: InputSubGraph!): SubGraph! @auth(organization: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -663,6 +714,49 @@ func (ec *executionContext) field_Mutation_addOrganization_args(ctx context.Cont
|
|||||||
return args, nil
|
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) {
|
func (ec *executionContext) field_Mutation_updateSubGraph_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
|
||||||
var err error
|
var err error
|
||||||
args := map[string]any{}
|
args := map[string]any{}
|
||||||
@@ -1057,6 +1151,75 @@ func (ec *executionContext) fieldContext_Mutation_addOrganization(ctx context.Co
|
|||||||
return fc, nil
|
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) {
|
func (ec *executionContext) _Mutation_addAPIKey(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||||
return graphql.ResolveField(
|
return graphql.ResolveField(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -1132,6 +1295,134 @@ func (ec *executionContext) fieldContext_Mutation_addAPIKey(ctx context.Context,
|
|||||||
return fc, nil
|
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) {
|
func (ec *executionContext) _Mutation_updateSubGraph(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||||
return graphql.ResolveField(
|
return graphql.ResolveField(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -1400,6 +1691,63 @@ func (ec *executionContext) fieldContext_Query_organizations(_ context.Context,
|
|||||||
return fc, nil
|
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) {
|
func (ec *executionContext) _Query_supergraph(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||||
return graphql.ResolveField(
|
return graphql.ResolveField(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -1414,6 +1762,11 @@ func (ec *executionContext) _Query_supergraph(ctx context.Context, field graphql
|
|||||||
directive0 := next
|
directive0 := next
|
||||||
|
|
||||||
directive1 := func(ctx context.Context) (any, error) {
|
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)
|
organization, err := ec.unmarshalOBoolean2ᚖbool(ctx, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var zeroVal model.Supergraph
|
var zeroVal model.Supergraph
|
||||||
@@ -1423,7 +1776,7 @@ func (ec *executionContext) _Query_supergraph(ctx context.Context, field graphql
|
|||||||
var zeroVal model.Supergraph
|
var zeroVal model.Supergraph
|
||||||
return zeroVal, errors.New("directive auth is not implemented")
|
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
|
next = directive1
|
||||||
@@ -1473,6 +1826,11 @@ func (ec *executionContext) _Query_latestSchema(ctx context.Context, field graph
|
|||||||
directive0 := next
|
directive0 := next
|
||||||
|
|
||||||
directive1 := func(ctx context.Context) (any, error) {
|
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)
|
organization, err := ec.unmarshalOBoolean2ᚖbool(ctx, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var zeroVal *model.SchemaUpdate
|
var zeroVal *model.SchemaUpdate
|
||||||
@@ -1482,7 +1840,7 @@ func (ec *executionContext) _Query_latestSchema(ctx context.Context, field graph
|
|||||||
var zeroVal *model.SchemaUpdate
|
var zeroVal *model.SchemaUpdate
|
||||||
return zeroVal, errors.New("directive auth is not implemented")
|
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
|
next = directive1
|
||||||
@@ -3938,6 +4296,13 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
|
|||||||
if out.Values[i] == graphql.Null {
|
if out.Values[i] == graphql.Null {
|
||||||
out.Invalids++
|
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":
|
case "addAPIKey":
|
||||||
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
|
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
|
||||||
return ec._Mutation_addAPIKey(ctx, field)
|
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 {
|
if out.Values[i] == graphql.Null {
|
||||||
out.Invalids++
|
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":
|
case "updateSubGraph":
|
||||||
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
|
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
|
||||||
return ec._Mutation_updateSubGraph(ctx, field)
|
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) })
|
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) })
|
out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) })
|
||||||
case "supergraph":
|
case "supergraph":
|
||||||
field := field
|
field := field
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
type Query {
|
type Query {
|
||||||
organizations: [Organization!]! @auth(user: true)
|
organizations: [Organization!]! @auth(user: true)
|
||||||
supergraph(ref: String!, isAfter: String): Supergraph! @auth(organization: true)
|
allOrganizations: [Organization!]! @auth(user: true)
|
||||||
latestSchema(ref: String!): SchemaUpdate! @auth(organization: true)
|
supergraph(ref: String!, isAfter: String): Supergraph! @auth(user: true, organization: true)
|
||||||
|
latestSchema(ref: String!): SchemaUpdate! @auth(user: true, organization: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
addOrganization(name: String!): Organization! @auth(user: true)
|
addOrganization(name: String!): Organization! @auth(user: true)
|
||||||
|
addUserToOrganization(organizationId: ID!, userId: String!): Organization! @auth(user: true)
|
||||||
addAPIKey(input: InputAPIKey): APIKey! @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)
|
updateSubGraph(input: InputSubGraph!): SubGraph! @auth(organization: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,24 @@ func (r *mutationResolver) AddOrganization(ctx context.Context, name string) (*m
|
|||||||
return ToGqlOrganization(*org), nil
|
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.
|
// AddAPIKey is the resolver for the addAPIKey field.
|
||||||
func (r *mutationResolver) AddAPIKey(ctx context.Context, input *model.InputAPIKey) (*model.APIKey, error) {
|
func (r *mutationResolver) AddAPIKey(ctx context.Context, input *model.InputAPIKey) (*model.APIKey, error) {
|
||||||
sub := middleware.UserFromContext(ctx)
|
sub := middleware.UserFromContext(ctx)
|
||||||
@@ -71,6 +89,41 @@ func (r *mutationResolver) AddAPIKey(ctx context.Context, input *model.InputAPIK
|
|||||||
}, nil
|
}, 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.
|
// UpdateSubGraph is the resolver for the updateSubGraph field.
|
||||||
func (r *mutationResolver) UpdateSubGraph(ctx context.Context, input model.InputSubGraph) (*model.SubGraph, error) {
|
func (r *mutationResolver) UpdateSubGraph(ctx context.Context, input model.InputSubGraph) (*model.SubGraph, error) {
|
||||||
orgId := middleware.OrganizationFromContext(ctx)
|
orgId := middleware.OrganizationFromContext(ctx)
|
||||||
@@ -183,13 +236,49 @@ func (r *queryResolver) Organizations(ctx context.Context) ([]*model.Organizatio
|
|||||||
return ToGqlOrganizations(orgs), nil
|
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.
|
// Supergraph is the resolver for the supergraph field.
|
||||||
func (r *queryResolver) Supergraph(ctx context.Context, ref string, isAfter *string) (model.Supergraph, error) {
|
func (r *queryResolver) Supergraph(ctx context.Context, ref string, isAfter *string) (model.Supergraph, error) {
|
||||||
orgId := middleware.OrganizationFromContext(ctx)
|
orgId := middleware.OrganizationFromContext(ctx)
|
||||||
|
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)
|
_, err := r.apiKeyCanAccessRef(ctx, ref, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
r.Logger.Error("API key cannot access ref", "error", err, "ref", ref)
|
||||||
return nil, err
|
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 := ""
|
after := ""
|
||||||
if isAfter != nil {
|
if isAfter != nil {
|
||||||
after = *isAfter
|
after = *isAfter
|
||||||
@@ -241,17 +330,35 @@ func (r *queryResolver) Supergraph(ctx context.Context, ref string, isAfter *str
|
|||||||
// LatestSchema is the resolver for the latestSchema field.
|
// LatestSchema is the resolver for the latestSchema field.
|
||||||
func (r *queryResolver) LatestSchema(ctx context.Context, ref string) (*model.SchemaUpdate, error) {
|
func (r *queryResolver) LatestSchema(ctx context.Context, ref string) (*model.SchemaUpdate, error) {
|
||||||
orgId := middleware.OrganizationFromContext(ctx)
|
orgId := middleware.OrganizationFromContext(ctx)
|
||||||
|
userId := middleware.UserFromContext(ctx)
|
||||||
|
|
||||||
r.Logger.Info("LatestSchema query",
|
r.Logger.Info("LatestSchema query",
|
||||||
"ref", ref,
|
"ref", ref,
|
||||||
"orgId", orgId,
|
"orgId", orgId,
|
||||||
|
"userId", userId,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// If authenticated with API key (organization), check access
|
||||||
|
if orgId != "" {
|
||||||
_, err := r.apiKeyCanAccessRef(ctx, ref, false)
|
_, err := r.apiKeyCanAccessRef(ctx, ref, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.Logger.Error("API key cannot access ref", "error", err, "ref", ref)
|
r.Logger.Error("API key cannot access ref", "error", err, "ref", ref)
|
||||||
return nil, err
|
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
|
// Get current services and schema
|
||||||
services, lastUpdate := r.Cache.Services(orgId, ref, "")
|
services, lastUpdate := r.Cache.Services(orgId, ref, "")
|
||||||
|
|||||||
+62
-4
@@ -67,6 +67,37 @@ func UserFromContext(ctx context.Context) string {
|
|||||||
return ""
|
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 {
|
func OrganizationFromContext(ctx context.Context) string {
|
||||||
if value := ctx.Value(OrganizationKey); value != nil {
|
if value := ctx.Value(OrganizationKey); value != nil {
|
||||||
if u, ok := value.(domain.Organization); ok {
|
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) {
|
func (m *AuthMiddleware) Directive(ctx context.Context, _ interface{}, next graphql.Resolver, user *bool, organization *bool) (res interface{}, err error) {
|
||||||
if user != nil && *user {
|
userRequired := user != nil && *user
|
||||||
if u := UserFromContext(ctx); u == "" {
|
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")
|
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")
|
return nil, fmt.Errorf("no organization available in request")
|
||||||
}
|
}
|
||||||
|
fmt.Printf("[Auth Directive] ACCEPTED: Organization authenticated\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
return next(ctx)
|
return next(ctx)
|
||||||
}
|
}
|
||||||
|
|||||||
+104
-4
@@ -427,7 +427,10 @@ func TestAuthMiddleware_Directive_RequiresBoth(t *testing.T) {
|
|||||||
Name: "Test Org",
|
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(context.Background(), UserKey, "user-123")
|
||||||
ctx = context.WithValue(ctx, OrganizationKey, org)
|
ctx = context.WithValue(ctx, OrganizationKey, org)
|
||||||
_, err := authMiddleware.Directive(ctx, nil, func(ctx context.Context) (interface{}, error) {
|
_, err := authMiddleware.Directive(ctx, nil, func(ctx context.Context) (interface{}, error) {
|
||||||
@@ -435,19 +438,27 @@ func TestAuthMiddleware_Directive_RequiresBoth(t *testing.T) {
|
|||||||
}, &requireUser, &requireOrg)
|
}, &requireUser, &requireOrg)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// Test with only user
|
// Test with only user - should succeed (OR logic)
|
||||||
ctx = context.WithValue(context.Background(), UserKey, "user-123")
|
ctx = context.WithValue(context.Background(), UserKey, "user-123")
|
||||||
_, err = authMiddleware.Directive(ctx, nil, func(ctx context.Context) (interface{}, error) {
|
_, err = authMiddleware.Directive(ctx, nil, func(ctx context.Context) (interface{}, error) {
|
||||||
return "success", nil
|
return "success", nil
|
||||||
}, &requireUser, &requireOrg)
|
}, &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)
|
ctx = context.WithValue(context.Background(), OrganizationKey, org)
|
||||||
_, err = authMiddleware.Directive(ctx, nil, func(ctx context.Context) (interface{}, error) {
|
_, err = authMiddleware.Directive(ctx, nil, func(ctx context.Context) (interface{}, error) {
|
||||||
return "success", nil
|
return "success", nil
|
||||||
}, &requireUser, &requireOrg)
|
}, &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.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "authentication required")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAuthMiddleware_Directive_NoRequirements(t *testing.T) {
|
func TestAuthMiddleware_Directive_NoRequirements(t *testing.T) {
|
||||||
@@ -462,3 +473,92 @@ func TestAuthMiddleware_Directive_NoRequirements(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "success", result)
|
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
|
package sdlmerge
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -61,12 +62,13 @@ func MergeSDLs(SDLs ...string) (string, error) {
|
|||||||
return "", fmt.Errorf("merge ast: %w", err)
|
return "", fmt.Errorf("merge ast: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
out, err := astprinter.PrintString(&doc)
|
// Format with indentation for better readability
|
||||||
if err != nil {
|
buf := &bytes.Buffer{}
|
||||||
|
if err := astprinter.PrintIndent(&doc, []byte(" "), buf); err != nil {
|
||||||
return "", fmt.Errorf("stringify schema: %w", err)
|
return "", fmt.Errorf("stringify schema: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return out, nil
|
return buf.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateSubgraphs(subgraphs []string) error {
|
func validateSubgraphs(subgraphs []string) error {
|
||||||
|
|||||||
Reference in New Issue
Block a user