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:
+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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user