Merged in feature/textExtractionsPart4 (pull request #197)
implement GET /identity * GET /identity
This commit is contained in:
@@ -58,6 +58,21 @@ func extractToken(c echo.Context, config auth.ConfigProvider) (string, error) {
|
||||
return tokenCookie.Value, nil
|
||||
}
|
||||
|
||||
// isAuthOnlyPath returns true for paths that require authentication but NOT authorization.
|
||||
// These endpoints are accessible to all authenticated users regardless of their Permit.io roles.
|
||||
// This enables users to discover their own roles/permissions before making other API calls.
|
||||
func isAuthOnlyPath(requestPath string) bool {
|
||||
authOnlyPaths := []string{
|
||||
"/identity", // Users need to discover their roles without having any specific role
|
||||
}
|
||||
for _, path := range authOnlyPaths {
|
||||
if requestPath == path {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// performPermitIOAuthorization handles the Permit.io authorization check
|
||||
func performPermitIOAuthorization(c echo.Context, requestPath string, config auth.ConfigProvider) error {
|
||||
// Skip authorization for swagger and metrics
|
||||
@@ -65,6 +80,11 @@ func performPermitIOAuthorization(c echo.Context, requestPath string, config aut
|
||||
return nil
|
||||
}
|
||||
|
||||
// Skip authorization for auth-only paths (authentication required, but no specific permissions needed)
|
||||
if isAuthOnlyPath(requestPath) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get user subject from JWT for Permit.io
|
||||
userSubject, ok := GetUserSubject(c)
|
||||
if !ok {
|
||||
|
||||
@@ -312,3 +312,54 @@ func TestTokenValidationMiddleware(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestIsAuthOnlyPath verifies that certain paths bypass authorization while still requiring authentication.
|
||||
// The /identity endpoint should be accessible to all authenticated users regardless of their Permit.io roles,
|
||||
// allowing them to discover their own roles/permissions.
|
||||
func TestIsAuthOnlyPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "identity endpoint should bypass authorization",
|
||||
path: "/identity",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "api endpoint should require authorization",
|
||||
path: "/api/clients",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "documents endpoint should require authorization",
|
||||
path: "/documents",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "health endpoint is not auth-only (it's public)",
|
||||
path: "/health",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "similar but different path should require authorization",
|
||||
path: "/identity/something",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "prefix match should not work",
|
||||
path: "/identityinfo",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := isAuthOnlyPath(tt.path)
|
||||
if result != tt.expected {
|
||||
t.Errorf("isAuthOnlyPath(%q) = %v, expected %v", tt.path, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,22 +3,18 @@ package cognitoauth
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ValidatePermitIOConfiguration validates both PDP connectivity and API key at startup
|
||||
// ValidatePermitIOConfiguration validates PDP connectivity and API key at startup
|
||||
// This function should be called during application startup to ensure configuration is correct
|
||||
// before processing any real authorization requests.
|
||||
//
|
||||
// It performs two checks:
|
||||
// 1. PDP Health Check: Tests connectivity to the PDP URL using the /healthy endpoint
|
||||
// 2. API Key Validation: Makes a dummy permission check to verify the API key is valid
|
||||
// It makes a dummy permission check to verify the API key is valid and PDP is reachable.
|
||||
//
|
||||
// If DISABLE_AUTH=true, validation is skipped and function returns success.
|
||||
// Returns an error if either check fails, which should cause the application to halt startup.
|
||||
// Returns an error if the check fails, which should cause the application to halt startup.
|
||||
func ValidatePermitIOConfiguration() error {
|
||||
// Skip validation if auth is disabled
|
||||
disableAuth := os.Getenv("DISABLE_AUTH")
|
||||
@@ -27,10 +23,10 @@ func ValidatePermitIOConfiguration() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
pdpURL := os.Getenv("PERMIT_IO_PDP_URL")
|
||||
apiKey := os.Getenv("PERMIT_IO_API_KEY")
|
||||
pdpURL := strings.TrimSpace(os.Getenv("PERMIT_IO_PDP_URL"))
|
||||
apiKey := strings.TrimSpace(os.Getenv("PERMIT_IO_API_KEY"))
|
||||
|
||||
// Apply default for PDP URL if not set
|
||||
// Apply default for PDP URL if not set or whitespace-only
|
||||
if pdpURL == "" {
|
||||
pdpURL = "http://localhost:7766"
|
||||
}
|
||||
@@ -39,26 +35,7 @@ func ValidatePermitIOConfiguration() error {
|
||||
return fmt.Errorf("PERMIT_IO_API_KEY environment variable must be set")
|
||||
}
|
||||
|
||||
// 1. Test PDP connectivity with /healthy endpoint
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
healthURL := strings.TrimSuffix(pdpURL, "/") + "/healthy"
|
||||
|
||||
req, err := http.NewRequest("GET", healthURL, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create health check request: %v", err)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Permit.io PDP connectivity failed with PDP_URL=%s: %v", pdpURL, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return fmt.Errorf("Permit.io PDP health check failed with PDP_URL=%s: HTTP %d", pdpURL, resp.StatusCode)
|
||||
}
|
||||
|
||||
// 2. Test API key validity with a dummy permission check
|
||||
// Test API key validity and PDP connectivity with a dummy permission check
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelWarn}))
|
||||
permitClient := NewPermitIOClient(apiKey, pdpURL, logger)
|
||||
|
||||
@@ -67,7 +44,7 @@ func ValidatePermitIOConfiguration() error {
|
||||
testAction := "read"
|
||||
testResource := "startup-validation-resource"
|
||||
|
||||
_, err = permitClient.CheckPermission(testUser, testAction, testResource)
|
||||
_, err := permitClient.CheckPermission(testUser, testAction, testResource)
|
||||
if err != nil {
|
||||
// Check for specific error patterns to distinguish error types
|
||||
errStr := err.Error()
|
||||
|
||||
@@ -75,7 +75,7 @@ func TestValidatePermitIOConfiguration_Unit(t *testing.T) {
|
||||
err := ValidatePermitIOConfiguration()
|
||||
assert.Error(t, err)
|
||||
// Should fail on connectivity, not API key validation
|
||||
assert.Contains(t, err.Error(), "PDP connectivity failed")
|
||||
assert.Contains(t, err.Error(), "Permit.io PDP connectivity failed")
|
||||
})
|
||||
|
||||
t.Run("AuthEnabledWithDefaultURL", func(t *testing.T) {
|
||||
@@ -91,7 +91,7 @@ func TestValidatePermitIOConfiguration_Unit(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("HealthCheckConnectivityError", func(t *testing.T) {
|
||||
// Test health check connectivity failure
|
||||
// Test connectivity failure (health check was removed, now uses CheckPermission)
|
||||
t.Setenv("DISABLE_AUTH", "false")
|
||||
t.Setenv("PERMIT_IO_API_KEY", "test-api-key-12345")
|
||||
// Use an invalid host that will fail to connect
|
||||
@@ -100,25 +100,25 @@ func TestValidatePermitIOConfiguration_Unit(t *testing.T) {
|
||||
err := ValidatePermitIOConfiguration()
|
||||
assert.Error(t, err)
|
||||
// Should fail on connectivity
|
||||
assert.Contains(t, err.Error(), "PDP connectivity failed")
|
||||
assert.Contains(t, err.Error(), "Permit.io PDP connectivity failed")
|
||||
})
|
||||
|
||||
t.Run("HealthCheckNon200Response", func(t *testing.T) {
|
||||
// Test health check with non-200 response (hard to simulate without server)
|
||||
// This tests the path where connectivity works but PDP returns non-200
|
||||
// Test with external service that returns non-200 response
|
||||
// Health check was removed, now uses CheckPermission which will fail on invalid responses
|
||||
t.Setenv("DISABLE_AUTH", "false")
|
||||
t.Setenv("PERMIT_IO_API_KEY", "test-api-key-12345")
|
||||
// Use localhost with a port that might return different status
|
||||
// Use httpbin which will return 500 but won't speak Permit protocol
|
||||
t.Setenv("PERMIT_IO_PDP_URL", "http://httpbin.org/status/500")
|
||||
|
||||
err := ValidatePermitIOConfiguration()
|
||||
// This will likely fail on connectivity, but if it connects, should test non-200 path
|
||||
// Should fail with some error (connectivity or validation)
|
||||
if err != nil {
|
||||
// Accept either connectivity failure or health check failure
|
||||
// Accept various failure modes since external service may behave differently
|
||||
assert.True(t,
|
||||
strings.Contains(err.Error(), "PDP connectivity failed") ||
|
||||
strings.Contains(err.Error(), "health check failed"),
|
||||
"Should fail on either connectivity or health check")
|
||||
strings.Contains(err.Error(), "Permit.io") ||
|
||||
strings.Contains(err.Error(), "validation failed"),
|
||||
"Should fail on either connectivity or validation, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -155,7 +155,7 @@ func TestValidatePermitIOConfiguration_Unit(t *testing.T) {
|
||||
err := ValidatePermitIOConfiguration()
|
||||
assert.Error(t, err)
|
||||
// Should fail on connectivity
|
||||
assert.Contains(t, err.Error(), "PDP connectivity failed")
|
||||
assert.Contains(t, err.Error(), "Permit.io PDP connectivity failed")
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -178,20 +178,21 @@ func TestValidatePermitIOConfiguration_Unit(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("TestHTTPStatusPath", func(t *testing.T) {
|
||||
// Test that health check properly handles HTTP status codes
|
||||
// Test validation with external service returning non-200 status
|
||||
// Health check was removed, now uses CheckPermission for validation
|
||||
t.Setenv("DISABLE_AUTH", "false")
|
||||
t.Setenv("PERMIT_IO_API_KEY", "test-api-key-12345")
|
||||
// Use a URL that will return a specific HTTP status for health checks
|
||||
// Use a URL that will return 404 but won't speak Permit protocol
|
||||
t.Setenv("PERMIT_IO_PDP_URL", "https://httpbin.org/status/404")
|
||||
|
||||
err := ValidatePermitIOConfiguration()
|
||||
assert.Error(t, err)
|
||||
// Should fail, but external service may be unreliable
|
||||
// Accept either health check failure or connectivity failure
|
||||
// Should fail with some validation error
|
||||
// Accept various failure modes since external service may behave differently
|
||||
assert.True(t,
|
||||
strings.Contains(err.Error(), "health check failed") ||
|
||||
strings.Contains(err.Error(), "PDP connectivity failed"),
|
||||
"Should fail on either health check or connectivity, got: %v", err)
|
||||
strings.Contains(err.Error(), "Permit.io") ||
|
||||
strings.Contains(err.Error(), "validation failed"),
|
||||
"Should fail on validation, got: %v", err)
|
||||
})
|
||||
|
||||
t.Run("EmptyAPIKeyAfterDefault", func(t *testing.T) {
|
||||
@@ -283,7 +284,7 @@ func TestValidatePermitIOConfiguration_Unit(t *testing.T) {
|
||||
name: "UnexpectedErrorPattern",
|
||||
apiKey: "test-unexpected-error-key",
|
||||
pdpURL: "http://invalid-host-that-will-refuse:9999",
|
||||
expectError: "PDP connectivity failed",
|
||||
expectError: "Permit.io PDP connectivity failed",
|
||||
},
|
||||
{
|
||||
name: "UnauthorizedPattern",
|
||||
@@ -310,8 +311,9 @@ func TestValidatePermitIOConfiguration_Unit(t *testing.T) {
|
||||
t.Run("SpecificErrorBranchCoverage", func(t *testing.T) {
|
||||
// Test specific error patterns that should trigger different error handling branches
|
||||
// These tests focus on exercising the error pattern matching logic
|
||||
// Note: Health check was removed, validation now uses CheckPermission directly
|
||||
|
||||
// Test malformed URL that causes http.NewRequest to fail
|
||||
// Test malformed URL that causes request creation to fail
|
||||
t.Run("MalformedHealthURL", func(t *testing.T) {
|
||||
t.Setenv("DISABLE_AUTH", "false")
|
||||
t.Setenv("PERMIT_IO_API_KEY", "test-key-12345")
|
||||
@@ -319,11 +321,11 @@ func TestValidatePermitIOConfiguration_Unit(t *testing.T) {
|
||||
|
||||
err := ValidatePermitIOConfiguration()
|
||||
assert.Error(t, err)
|
||||
// Should fail on request creation
|
||||
assert.Contains(t, err.Error(), "failed to create health check request")
|
||||
// Should fail on request creation - permit client will fail to parse URL
|
||||
assert.Contains(t, err.Error(), "Permit.io validation failed")
|
||||
})
|
||||
|
||||
// Test URL with invalid scheme that fails before health check
|
||||
// Test URL with invalid scheme that fails before validation
|
||||
t.Run("InvalidScheme", func(t *testing.T) {
|
||||
t.Setenv("DISABLE_AUTH", "false")
|
||||
t.Setenv("PERMIT_IO_API_KEY", "test-key-12345")
|
||||
@@ -344,8 +346,12 @@ func TestValidatePermitIOConfiguration_Unit(t *testing.T) {
|
||||
|
||||
err := ValidatePermitIOConfiguration()
|
||||
assert.Error(t, err)
|
||||
// Should fail on connectivity
|
||||
assert.Contains(t, err.Error(), "PDP connectivity failed")
|
||||
// Should fail - may be connectivity or timeout error
|
||||
// The error message format changed after removing health check
|
||||
assert.True(t,
|
||||
strings.Contains(err.Error(), "Permit.io") ||
|
||||
strings.Contains(err.Error(), "timeout"),
|
||||
"Should fail on connectivity or timeout, got: %v", err)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -589,14 +595,16 @@ func TestValidatePermitIOConfiguration_Unit(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("WhitespaceHandling", func(t *testing.T) {
|
||||
// Test that whitespace-only values are properly trimmed
|
||||
// This prevents a panic in the permit-golang library
|
||||
t.Setenv("DISABLE_AUTH", "false")
|
||||
t.Setenv("PERMIT_IO_API_KEY", " ")
|
||||
t.Setenv("PERMIT_IO_PDP_URL", " ")
|
||||
t.Setenv("PERMIT_IO_API_KEY", " ") // Whitespace only - should be trimmed to empty
|
||||
t.Setenv("PERMIT_IO_PDP_URL", " ") // Whitespace only - should use default after trim
|
||||
|
||||
err := ValidatePermitIOConfiguration()
|
||||
assert.Error(t, err)
|
||||
// Should fail on connectivity since whitespace URL is invalid
|
||||
assert.NotEmpty(t, err.Error())
|
||||
// After trimming, API key becomes empty so should fail with missing API key error
|
||||
assert.Contains(t, err.Error(), "PERMIT_IO_API_KEY environment variable must be set")
|
||||
})
|
||||
|
||||
t.Run("LocalhostDefaultBehavior", func(t *testing.T) {
|
||||
|
||||
@@ -23,7 +23,16 @@ import (
|
||||
// Returns:
|
||||
// - error: Error if the user creation fails. Returns nil if user is created or already exists.
|
||||
func CreatePermitUser(client *http.Client, config *PermitConfig, cognitoUser *CognitoUserResponse) error {
|
||||
url := fmt.Sprintf("%s/v2/facts/%s/%s/users", config.BaseURL, config.ProjectID, config.EnvID)
|
||||
projectID, err := config.GetProjectID(client)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get project ID: %w", err)
|
||||
}
|
||||
envID, err := config.GetEnvID(client)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get environment ID: %w", err)
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/v2/facts/%s/%s/users", config.BaseURL, projectID, envID)
|
||||
|
||||
userObj := PermitUser{
|
||||
Key: cognitoUser.SubjectID, // Use Cognito Subject ID as the key
|
||||
@@ -95,7 +104,16 @@ func CreatePermitUser(client *http.Client, config *PermitConfig, cognitoUser *Co
|
||||
// Returns:
|
||||
// - error: Error if the deletion fails
|
||||
func DeletePermitUser(client *http.Client, config *PermitConfig, userKey string) error {
|
||||
url := fmt.Sprintf("%s/v2/facts/%s/%s/users/%s", config.BaseURL, config.ProjectID, config.EnvID, userKey)
|
||||
projectID, err := config.GetProjectID(client)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get project ID: %w", err)
|
||||
}
|
||||
envID, err := config.GetEnvID(client)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get environment ID: %w", err)
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/v2/facts/%s/%s/users/%s", config.BaseURL, projectID, envID, userKey)
|
||||
|
||||
req, err := http.NewRequest("DELETE", url, nil)
|
||||
if err != nil {
|
||||
@@ -139,8 +157,17 @@ func DeletePermitUser(client *http.Client, config *PermitConfig, userKey string)
|
||||
// - string: The Permit.io user key if found, empty string if not found
|
||||
// - error: Error if the search fails
|
||||
func FindPermitUserByEmail(client *http.Client, config *PermitConfig, email string) (string, error) {
|
||||
projectID, err := config.GetProjectID(client)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get project ID: %w", err)
|
||||
}
|
||||
envID, err := config.GetEnvID(client)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get environment ID: %w", err)
|
||||
}
|
||||
|
||||
// Search for user by email
|
||||
url := fmt.Sprintf("%s/v2/facts/%s/%s/users?search=%s", config.BaseURL, config.ProjectID, config.EnvID, email)
|
||||
url := fmt.Sprintf("%s/v2/facts/%s/%s/users?search=%s", config.BaseURL, projectID, envID, email)
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
@@ -233,7 +260,16 @@ func UserExistsInPermit(client *http.Client, config *PermitConfig, email string)
|
||||
// Returns:
|
||||
// - error: Error if the role assignment fails. Returns nil if role is assigned or already assigned.
|
||||
func AssignRole(client *http.Client, config *PermitConfig, userID, role string) error {
|
||||
url := fmt.Sprintf("%s/v2/facts/%s/%s/role_assignments", config.BaseURL, config.ProjectID, config.EnvID)
|
||||
projectID, err := config.GetProjectID(client)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get project ID: %w", err)
|
||||
}
|
||||
envID, err := config.GetEnvID(client)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get environment ID: %w", err)
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/v2/facts/%s/%s/role_assignments", config.BaseURL, projectID, envID)
|
||||
|
||||
tenant := config.DefaultTenant
|
||||
if tenant == "" {
|
||||
@@ -298,7 +334,16 @@ func AssignRole(client *http.Client, config *PermitConfig, userID, role string)
|
||||
// Returns:
|
||||
// - error: Error if the role unassignment fails. Returns nil if role is unassigned or was already not assigned.
|
||||
func UnassignRole(client *http.Client, config *PermitConfig, userID, role string) error {
|
||||
url := fmt.Sprintf("%s/v2/facts/%s/%s/role_assignments", config.BaseURL, config.ProjectID, config.EnvID)
|
||||
projectID, err := config.GetProjectID(client)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get project ID: %w", err)
|
||||
}
|
||||
envID, err := config.GetEnvID(client)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get environment ID: %w", err)
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/v2/facts/%s/%s/role_assignments", config.BaseURL, projectID, envID)
|
||||
|
||||
tenant := config.DefaultTenant
|
||||
if tenant == "" {
|
||||
@@ -363,8 +408,17 @@ func UnassignRole(client *http.Client, config *PermitConfig, userID, role string
|
||||
// - []string: List of role keys assigned to the user
|
||||
// - error: Error if the retrieval fails
|
||||
func GetPermitUserRoles(client *http.Client, config *PermitConfig, userKey string) ([]string, error) {
|
||||
projectID, err := config.GetProjectID(client)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get project ID: %w", err)
|
||||
}
|
||||
envID, err := config.GetEnvID(client)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get environment ID: %w", err)
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/v2/facts/%s/%s/role_assignments?user=%s",
|
||||
config.BaseURL, config.ProjectID, config.EnvID, userKey)
|
||||
config.BaseURL, projectID, envID, userKey)
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
@@ -436,8 +490,17 @@ func GetPermitUserRoles(client *http.Client, config *PermitConfig, userKey strin
|
||||
// - []string: List of role keys that exist in the Permit.io environment
|
||||
// - error: Error if the retrieval fails
|
||||
func GetExistingRoles(client *http.Client, config *PermitConfig) ([]string, error) {
|
||||
projectID, err := config.GetProjectID(client)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get project ID: %w", err)
|
||||
}
|
||||
envID, err := config.GetEnvID(client)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get environment ID: %w", err)
|
||||
}
|
||||
|
||||
// Construct API URL for roles endpoint
|
||||
url := fmt.Sprintf("%s/v2/schema/%s/%s/roles", config.BaseURL, config.ProjectID, config.EnvID)
|
||||
url := fmt.Sprintf("%s/v2/schema/%s/%s/roles", config.BaseURL, projectID, envID)
|
||||
|
||||
// Create HTTP request
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
@@ -569,3 +632,123 @@ func ValidateRoles(client *http.Client, config *PermitConfig, requestedRoles []s
|
||||
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
// RoleDetails contains the details of a role including its permissions.
|
||||
type RoleDetails struct {
|
||||
Key string // The role key/identifier
|
||||
Name string // Human-readable role name
|
||||
Description string // Optional description of the role
|
||||
Permissions []string // List of permissions granted by this role
|
||||
}
|
||||
|
||||
// GetAPIKeyScope retrieves the scope information for a Permit.io API key.
|
||||
// This allows deriving the project and environment IDs from the API key itself,
|
||||
// eliminating the need for separate PERMIT_IO_PROJECT_ID and PERMIT_IO_ENV_ID
|
||||
// environment variables.
|
||||
//
|
||||
// Parameters:
|
||||
// - client: HTTP client for making API requests
|
||||
// - apiKey: The Permit.io API key
|
||||
// - baseURL: The Permit.io API base URL (e.g., https://api.permit.io)
|
||||
//
|
||||
// Returns:
|
||||
// - *APIKeyScope: The scope containing organization, project, and environment IDs
|
||||
// - error: Error if the API call fails or response cannot be parsed
|
||||
func GetAPIKeyScope(client *http.Client, apiKey, baseURL string) (*APIKeyScope, error) {
|
||||
url := fmt.Sprintf("%s/v2/api-key/scope", baseURL)
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error making request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading response: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var scope APIKeyScope
|
||||
if err := json.Unmarshal(body, &scope); err != nil {
|
||||
return nil, fmt.Errorf("error parsing response: %w", err)
|
||||
}
|
||||
|
||||
return &scope, nil
|
||||
}
|
||||
|
||||
// GetRoleDetails retrieves the details of a role from Permit.io, including its permissions.
|
||||
// Uses the schema API endpoint to get role definitions.
|
||||
//
|
||||
// Parameters:
|
||||
// - client: HTTP client for making API requests
|
||||
// - config: Permit.io configuration containing API credentials and project details
|
||||
// - roleKey: The role key to retrieve details for
|
||||
//
|
||||
// Returns:
|
||||
// - *RoleDetails: The role details including permissions
|
||||
// - error: Error if the retrieval fails
|
||||
func GetRoleDetails(client *http.Client, config *PermitConfig, roleKey string) (*RoleDetails, error) {
|
||||
projectID, err := config.GetProjectID(client)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get project ID: %w", err)
|
||||
}
|
||||
envID, err := config.GetEnvID(client)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get environment ID: %w", err)
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/v2/schema/%s/%s/roles/%s",
|
||||
config.BaseURL, projectID, envID, roleKey)
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+config.APIKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error making request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading response: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var roleData struct {
|
||||
Key string `json:"key"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Permissions []string `json:"permissions"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, &roleData); err != nil {
|
||||
return nil, fmt.Errorf("error parsing response: %w", err)
|
||||
}
|
||||
|
||||
return &RoleDetails{
|
||||
Key: roleData.Key,
|
||||
Name: roleData.Name,
|
||||
Description: roleData.Description,
|
||||
Permissions: roleData.Permissions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,327 @@
|
||||
package usermanagement
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// createMockPermitServer creates a mock server with scope endpoint and optional additional handlers.
|
||||
// It returns the server and a PermitConfig configured to use it.
|
||||
func createMockPermitServer(t *testing.T, additionalHandlers map[string]http.HandlerFunc) (*httptest.Server, *PermitConfig) {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// Always add the scope handler
|
||||
mux.HandleFunc("/v2/api-key/scope", func(w http.ResponseWriter, r *http.Request) {
|
||||
response := map[string]string{
|
||||
"organization_id": "test-org",
|
||||
"project_id": "test-project",
|
||||
"environment_id": "test-env",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
err := json.NewEncoder(w).Encode(response)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
// Add any additional handlers
|
||||
for path, handler := range additionalHandlers {
|
||||
mux.HandleFunc(path, handler)
|
||||
}
|
||||
|
||||
server := httptest.NewServer(mux)
|
||||
|
||||
config := &PermitConfig{
|
||||
APIKey: "test-api-key",
|
||||
BaseURL: server.URL,
|
||||
}
|
||||
|
||||
return server, config
|
||||
}
|
||||
|
||||
// TestGetAPIKeyScope_Success tests successful API key scope retrieval
|
||||
func TestGetAPIKeyScope_Success(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "GET", r.Method)
|
||||
assert.Equal(t, "/v2/api-key/scope", r.URL.Path)
|
||||
assert.Equal(t, "Bearer test-api-key", r.Header.Get("Authorization"))
|
||||
|
||||
response := map[string]string{
|
||||
"organization_id": "org-123",
|
||||
"project_id": "proj-456",
|
||||
"environment_id": "env-789",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
err := json.NewEncoder(w).Encode(response)
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
scope, err := GetAPIKeyScope(server.Client(), "test-api-key", server.URL)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, scope)
|
||||
assert.Equal(t, "org-123", scope.OrganizationID)
|
||||
assert.Equal(t, "proj-456", scope.ProjectID)
|
||||
assert.Equal(t, "env-789", scope.EnvironmentID)
|
||||
}
|
||||
|
||||
// TestGetAPIKeyScope_OrgLevelKey tests API key with only organization scope
|
||||
func TestGetAPIKeyScope_OrgLevelKey(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
response := map[string]string{
|
||||
"organization_id": "org-123",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
err := json.NewEncoder(w).Encode(response)
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
scope, err := GetAPIKeyScope(server.Client(), "test-api-key", server.URL)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, scope)
|
||||
assert.Equal(t, "org-123", scope.OrganizationID)
|
||||
assert.Empty(t, scope.ProjectID)
|
||||
assert.Empty(t, scope.EnvironmentID)
|
||||
}
|
||||
|
||||
// TestGetAPIKeyScope_Unauthorized tests unauthorized API key
|
||||
func TestGetAPIKeyScope_Unauthorized(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
_, err := w.Write([]byte(`{"error": "Invalid API key"}`))
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
scope, err := GetAPIKeyScope(server.Client(), "invalid-key", server.URL)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, scope)
|
||||
assert.Contains(t, err.Error(), "API error (status 401)")
|
||||
}
|
||||
|
||||
// TestPermitConfig_GetProjectID tests the GetProjectID method
|
||||
func TestPermitConfig_GetProjectID(t *testing.T) {
|
||||
server, config := createMockPermitServer(t, nil)
|
||||
defer server.Close()
|
||||
|
||||
projectID, err := config.GetProjectID(server.Client())
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "test-project", projectID)
|
||||
}
|
||||
|
||||
// TestPermitConfig_GetEnvID tests the GetEnvID method
|
||||
func TestPermitConfig_GetEnvID(t *testing.T) {
|
||||
server, config := createMockPermitServer(t, nil)
|
||||
defer server.Close()
|
||||
|
||||
envID, err := config.GetEnvID(server.Client())
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "test-env", envID)
|
||||
}
|
||||
|
||||
// TestPermitConfig_GetProjectID_OrgLevelKey tests GetProjectID with org-level key
|
||||
func TestPermitConfig_GetProjectID_OrgLevelKey(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
response := map[string]string{
|
||||
"organization_id": "org-123",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
err := json.NewEncoder(w).Encode(response)
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
config := &PermitConfig{
|
||||
APIKey: "test-api-key",
|
||||
BaseURL: server.URL,
|
||||
}
|
||||
|
||||
projectID, err := config.GetProjectID(server.Client())
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, projectID)
|
||||
assert.ErrorIs(t, err, ErrOrgLevelKey)
|
||||
}
|
||||
|
||||
// TestGetRoleDetails_Success tests successful role details retrieval
|
||||
func TestGetRoleDetails_Success(t *testing.T) {
|
||||
handlers := map[string]http.HandlerFunc{
|
||||
"/v2/schema/test-project/test-env/roles/auditor": func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "GET", r.Method)
|
||||
assert.Equal(t, "Bearer test-api-key", r.Header.Get("Authorization"))
|
||||
|
||||
response := map[string]interface{}{
|
||||
"key": "auditor",
|
||||
"name": "Auditor",
|
||||
"description": "Read-only access to view documents",
|
||||
"permissions": []string{"document:read", "document:list", "export:read"},
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
err := json.NewEncoder(w).Encode(response)
|
||||
require.NoError(t, err)
|
||||
},
|
||||
}
|
||||
|
||||
server, config := createMockPermitServer(t, handlers)
|
||||
defer server.Close()
|
||||
|
||||
roleDetails, err := GetRoleDetails(server.Client(), config, "auditor")
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, roleDetails)
|
||||
assert.Equal(t, "auditor", roleDetails.Key)
|
||||
assert.Equal(t, "Auditor", roleDetails.Name)
|
||||
assert.Equal(t, "Read-only access to view documents", roleDetails.Description)
|
||||
assert.Equal(t, []string{"document:read", "document:list", "export:read"}, roleDetails.Permissions)
|
||||
}
|
||||
|
||||
// TestGetRoleDetails_EmptyPermissions tests role with no permissions
|
||||
func TestGetRoleDetails_EmptyPermissions(t *testing.T) {
|
||||
handlers := map[string]http.HandlerFunc{
|
||||
"/v2/schema/test-project/test-env/roles/viewer": func(w http.ResponseWriter, r *http.Request) {
|
||||
response := map[string]interface{}{
|
||||
"key": "viewer",
|
||||
"name": "Viewer",
|
||||
"description": "",
|
||||
"permissions": []string{},
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
err := json.NewEncoder(w).Encode(response)
|
||||
require.NoError(t, err)
|
||||
},
|
||||
}
|
||||
|
||||
server, config := createMockPermitServer(t, handlers)
|
||||
defer server.Close()
|
||||
|
||||
roleDetails, err := GetRoleDetails(server.Client(), config, "viewer")
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, roleDetails)
|
||||
assert.Equal(t, "viewer", roleDetails.Key)
|
||||
assert.Equal(t, "Viewer", roleDetails.Name)
|
||||
assert.Empty(t, roleDetails.Description)
|
||||
assert.Empty(t, roleDetails.Permissions)
|
||||
}
|
||||
|
||||
// TestGetRoleDetails_NilPermissions tests role with nil permissions field
|
||||
func TestGetRoleDetails_NilPermissions(t *testing.T) {
|
||||
handlers := map[string]http.HandlerFunc{
|
||||
"/v2/schema/test-project/test-env/roles/basic": func(w http.ResponseWriter, r *http.Request) {
|
||||
response := map[string]interface{}{
|
||||
"key": "basic",
|
||||
"name": "Basic User",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
err := json.NewEncoder(w).Encode(response)
|
||||
require.NoError(t, err)
|
||||
},
|
||||
}
|
||||
|
||||
server, config := createMockPermitServer(t, handlers)
|
||||
defer server.Close()
|
||||
|
||||
roleDetails, err := GetRoleDetails(server.Client(), config, "basic")
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, roleDetails)
|
||||
assert.Equal(t, "basic", roleDetails.Key)
|
||||
assert.Equal(t, "Basic User", roleDetails.Name)
|
||||
assert.Nil(t, roleDetails.Permissions)
|
||||
}
|
||||
|
||||
// TestGetRoleDetails_NotFound tests role not found error
|
||||
func TestGetRoleDetails_NotFound(t *testing.T) {
|
||||
handlers := map[string]http.HandlerFunc{
|
||||
"/v2/schema/test-project/test-env/roles/nonexistent": func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
_, err := w.Write([]byte(`{"error": "Role not found"}`))
|
||||
require.NoError(t, err)
|
||||
},
|
||||
}
|
||||
|
||||
server, config := createMockPermitServer(t, handlers)
|
||||
defer server.Close()
|
||||
|
||||
roleDetails, err := GetRoleDetails(server.Client(), config, "nonexistent")
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, roleDetails)
|
||||
assert.Contains(t, err.Error(), "API error (status 404)")
|
||||
}
|
||||
|
||||
// TestGetRoleDetails_Unauthorized tests unauthorized error
|
||||
func TestGetRoleDetails_Unauthorized(t *testing.T) {
|
||||
handlers := map[string]http.HandlerFunc{
|
||||
"/v2/schema/test-project/test-env/roles/auditor": func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
_, err := w.Write([]byte(`{"error": "Invalid API key"}`))
|
||||
require.NoError(t, err)
|
||||
},
|
||||
}
|
||||
|
||||
server, config := createMockPermitServer(t, handlers)
|
||||
defer server.Close()
|
||||
|
||||
roleDetails, err := GetRoleDetails(server.Client(), config, "auditor")
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, roleDetails)
|
||||
assert.Contains(t, err.Error(), "API error (status 401)")
|
||||
}
|
||||
|
||||
// TestGetRoleDetails_InvalidJSON tests invalid JSON response handling
|
||||
func TestGetRoleDetails_InvalidJSON(t *testing.T) {
|
||||
handlers := map[string]http.HandlerFunc{
|
||||
"/v2/schema/test-project/test-env/roles/auditor": func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, err := w.Write([]byte(`invalid json`))
|
||||
require.NoError(t, err)
|
||||
},
|
||||
}
|
||||
|
||||
server, config := createMockPermitServer(t, handlers)
|
||||
defer server.Close()
|
||||
|
||||
roleDetails, err := GetRoleDetails(server.Client(), config, "auditor")
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, roleDetails)
|
||||
assert.Contains(t, err.Error(), "error parsing response")
|
||||
}
|
||||
|
||||
// TestGetRoleDetails_ConnectionError tests connection error handling
|
||||
func TestGetRoleDetails_ConnectionError(t *testing.T) {
|
||||
config := &PermitConfig{
|
||||
APIKey: "test-api-key",
|
||||
BaseURL: "http://nonexistent-host:9999",
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
roleDetails, err := GetRoleDetails(client, config, "auditor")
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, roleDetails)
|
||||
// Error will be from GetProjectID trying to fetch scope
|
||||
assert.Contains(t, err.Error(), "failed to get project ID")
|
||||
}
|
||||
@@ -3,6 +3,17 @@
|
||||
// for user creation, deletion, role management, and audit logging.
|
||||
package usermanagement
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// ErrOrgLevelKey is returned when an organization-level API key is used.
|
||||
// Organization-level keys do not have project or environment scope and cannot be used
|
||||
// for operations that require project/environment context.
|
||||
var ErrOrgLevelKey = errors.New("API key must be project-level or environment-level; organization-level keys are not supported")
|
||||
|
||||
// CognitoUserResponse represents a user in AWS Cognito with all relevant attributes.
|
||||
// This type is returned when creating or retrieving users from Cognito.
|
||||
type CognitoUserResponse struct {
|
||||
@@ -56,18 +67,85 @@ type UserAttributes struct {
|
||||
LastName string
|
||||
}
|
||||
|
||||
// APIKeyScope represents the scope information returned by the Permit.io API key introspection endpoint.
|
||||
// It contains the organization, project, and environment IDs that the API key has access to.
|
||||
// Organization-level keys will have only OrganizationID set.
|
||||
// Project-level keys will have OrganizationID and ProjectID set.
|
||||
// Environment-level keys will have all three fields set.
|
||||
type APIKeyScope struct {
|
||||
OrganizationID string `json:"organization_id"`
|
||||
ProjectID string `json:"project_id,omitempty"`
|
||||
EnvironmentID string `json:"environment_id,omitempty"`
|
||||
}
|
||||
|
||||
// PermitConfig holds configuration for connecting to Permit.io.
|
||||
// ProjectID and EnvID are automatically derived from the API key scope on first use.
|
||||
type PermitConfig struct {
|
||||
// APIKey is the Permit.io API key
|
||||
APIKey string
|
||||
// ProjectID is the Permit.io project identifier
|
||||
ProjectID string
|
||||
// EnvID is the Permit.io environment identifier
|
||||
EnvID string
|
||||
// BaseURL is the Permit.io API base URL (default: https://api.permit.io)
|
||||
BaseURL string
|
||||
// DefaultTenant is the default tenant for role assignments (default: "default")
|
||||
DefaultTenant string
|
||||
|
||||
// Internal fields for caching the API key scope
|
||||
scope *APIKeyScope
|
||||
scopeOnce sync.Once
|
||||
scopeErr error
|
||||
}
|
||||
|
||||
// ensureScope fetches and caches the API key scope from Permit.io.
|
||||
// This is called internally by GetProjectID and GetEnvID.
|
||||
// Uses sync.Once to ensure the scope is only fetched once per config instance.
|
||||
//
|
||||
// Parameters:
|
||||
// - client: HTTP client for making API requests
|
||||
//
|
||||
// Returns:
|
||||
// - error: Error if the scope could not be fetched
|
||||
func (c *PermitConfig) ensureScope(client *http.Client) error {
|
||||
c.scopeOnce.Do(func() {
|
||||
c.scope, c.scopeErr = GetAPIKeyScope(client, c.APIKey, c.BaseURL)
|
||||
})
|
||||
return c.scopeErr
|
||||
}
|
||||
|
||||
// GetProjectID returns the project ID from the API key scope.
|
||||
// The scope is fetched and cached on first call.
|
||||
//
|
||||
// Parameters:
|
||||
// - client: HTTP client for making API requests
|
||||
//
|
||||
// Returns:
|
||||
// - string: The project ID
|
||||
// - error: Error if the scope could not be fetched or project ID is empty
|
||||
func (c *PermitConfig) GetProjectID(client *http.Client) (string, error) {
|
||||
if err := c.ensureScope(client); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if c.scope.ProjectID == "" {
|
||||
return "", ErrOrgLevelKey
|
||||
}
|
||||
return c.scope.ProjectID, nil
|
||||
}
|
||||
|
||||
// GetEnvID returns the environment ID from the API key scope.
|
||||
// The scope is fetched and cached on first call.
|
||||
//
|
||||
// Parameters:
|
||||
// - client: HTTP client for making API requests
|
||||
//
|
||||
// Returns:
|
||||
// - string: The environment ID
|
||||
// - error: Error if the scope could not be fetched or environment ID is empty
|
||||
func (c *PermitConfig) GetEnvID(client *http.Client) (string, error) {
|
||||
if err := c.ensureScope(client); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if c.scope.EnvironmentID == "" {
|
||||
return "", ErrOrgLevelKey
|
||||
}
|
||||
return c.scope.EnvironmentID, nil
|
||||
}
|
||||
|
||||
// CognitoConfig holds configuration for connecting to AWS Cognito.
|
||||
|
||||
Reference in New Issue
Block a user