Merged in feature/textExtractionsPart4 (pull request #197)

implement GET /identity

* GET /identity
This commit is contained in:
Jay Brown
2025-12-16 04:26:06 +00:00
parent ab7072ac90
commit dae7bd4cc6
19 changed files with 1811 additions and 315 deletions
+8 -13
View File
@@ -50,6 +50,8 @@ func getAdminUserForAudit(ctx echo.Context) (cognitoauth.UserInfo, error) {
// getPermitConfig creates a PermitConfig by reading environment variables.
// This centralizes Permit.io configuration to avoid code duplication.
// Note: ProjectID and EnvID are automatically derived from the API key scope
// when first needed, so they do not need to be set here.
//
// Parameters:
// - ctrl: The Controllers instance containing the API key from config
@@ -57,26 +59,19 @@ func getAdminUserForAudit(ctx echo.Context) (cognitoauth.UserInfo, error) {
// Returns:
// - *usermanagement.PermitConfig: The Permit.io configuration
func (ctrl *Controllers) getPermitConfig() *usermanagement.PermitConfig {
projectID := os.Getenv("PERMIT_IO_PROJECT_ID")
if projectID == "" {
projectID = "default"
}
envID := os.Getenv("PERMIT_IO_ENV_ID")
if envID == "" {
envID = "dev"
}
tenant := os.Getenv("PERMIT_IO_TENANT")
if tenant == "" {
tenant = "default"
}
baseURL := os.Getenv("PERMIT_IO_BASE_URL")
if baseURL == "" {
baseURL = "https://api.permit.io"
}
return &usermanagement.PermitConfig{
APIKey: ctrl.cfg.GetPermitIOAPIKey(),
ProjectID: projectID,
EnvID: envID,
BaseURL: "https://api.permit.io",
BaseURL: baseURL,
DefaultTenant: tenant,
}
}
+262 -220
View File
@@ -843,6 +843,27 @@ type IdMessage struct {
Id openapi_types.UUID `json:"id"`
}
// IdentityResponse Response containing the user's roles and their permissions
type IdentityResponse struct {
// Roles List of roles assigned to the user with their permissions
Roles []IdentityRole `json:"roles"`
}
// IdentityRole A role assigned to the user with its permissions
type IdentityRole struct {
// Description Optional description of the role
Description *string `json:"description,omitempty"`
// Key The role key/identifier
Key string `json:"key"`
// Name Human-readable role name
Name string `json:"name"`
// Permissions List of permissions granted by this role
Permissions []string `json:"permissions"`
}
// LabelApplication Request to apply a workflow label to a document
type LabelApplication struct {
// AppliedBy Email of user applying the label
@@ -1308,6 +1329,9 @@ type ServerInterface interface {
// Get the home page menu
// (GET /home)
GetHomePage(ctx echo.Context) error
// Get current user identity and roles
// (GET /identity)
GetIdentity(ctx echo.Context) error
// Get all documents with a specific label
// (GET /labels/{labelName}/documents)
GetDocumentsByLabel(ctx echo.Context, labelName string, params GetDocumentsByLabelParams) error
@@ -2066,6 +2090,17 @@ func (w *ServerInterfaceWrapper) GetHomePage(ctx echo.Context) error {
return err
}
// GetIdentity converts echo context to params.
func (w *ServerInterfaceWrapper) GetIdentity(ctx echo.Context) error {
var err error
ctx.Set(JwtAuthScopes, []string{})
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.GetIdentity(ctx)
return err
}
// GetDocumentsByLabel converts echo context to params.
func (w *ServerInterfaceWrapper) GetDocumentsByLabel(ctx echo.Context) error {
var err error
@@ -2280,6 +2315,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
router.GET(baseURL+"/folders/:folderId/documents", wrapper.GetFolderDocuments)
router.GET(baseURL+"/folders/:folderId/metrics", wrapper.GetFolderMetrics)
router.GET(baseURL+"/home", wrapper.GetHomePage)
router.GET(baseURL+"/identity", wrapper.GetIdentity)
router.GET(baseURL+"/labels/:labelName/documents", wrapper.GetDocumentsByLabel)
router.GET(baseURL+"/login", wrapper.Login)
router.GET(baseURL+"/login-callback", wrapper.LoginCallback)
@@ -2295,226 +2331,232 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
// Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{
"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",
"lDIqlSiPr3/PJnj/5f8EAAD//+2UaeGdXAEA",
}
// GetSwagger returns the content of the embedded swagger specification file
+6
View File
@@ -35,6 +35,12 @@ var endpointsList = []string{
// Export Service Endpoints
"/export",
// Identity Endpoint
"/identity",
// Swagger UI
"/swagger/index.html",
}
// homeHandler renders a simple HTML page with links to all endpoints
+89
View File
@@ -0,0 +1,89 @@
// identityHandlers.go implements the identity API handler for user role discovery.
// This handler allows authenticated users to discover their assigned roles and
// the permissions granted by those roles from Permit.io.
package queryapi
import (
"net/http"
"time"
"queryorchestration/internal/cognitoauth"
"queryorchestration/internal/usermanagement"
"github.com/labstack/echo/v4"
)
// GetIdentity returns the currently authenticated user's roles and permissions from Permit.io.
// GET /identity
//
// This endpoint requires authentication but no authorization - any authenticated
// user can retrieve their own roles and the permissions granted by those roles.
//
// Returns:
// - 200: IdentityResponse with roles array including permissions
// - 401: Unauthorized if user is not authenticated
// - 502: Bad gateway if Permit.io call fails
func (ctrl *Controllers) GetIdentity(ctx echo.Context) error {
// Extract user subject ID from JWT token
userSubject, ok := cognitoauth.GetUserSubject(ctx)
if !ok {
return ctx.JSON(http.StatusUnauthorized, ErrorMessage{
Message: "Could not identify user from token",
})
}
// Get role keys from Permit.io
httpClient := &http.Client{Timeout: 10 * time.Second}
permitConfig := ctrl.getPermitConfig()
roleKeys, err := usermanagement.GetPermitUserRoles(httpClient, permitConfig, userSubject)
if err != nil {
ctrl.cfg.GetAuthLogger().Error("Failed to get user roles from Permit.io",
"error", err,
"user_subject", userSubject)
return ctx.JSON(http.StatusBadGateway, ErrorMessage{
Message: "Failed to retrieve roles from authorization service",
})
}
// Build response with role details
roles := make([]IdentityRole, 0, len(roleKeys))
for _, roleKey := range roleKeys {
// Get role details including permissions
roleDetails, err := usermanagement.GetRoleDetails(httpClient, permitConfig, roleKey)
if err != nil {
ctrl.cfg.GetAuthLogger().Warn("Failed to get role details from Permit.io",
"error", err,
"role_key", roleKey)
// Include role with empty permissions if details fetch fails
roles = append(roles, IdentityRole{
Key: roleKey,
Name: roleKey,
Permissions: []string{},
})
continue
}
permissions := roleDetails.Permissions
if permissions == nil {
permissions = []string{}
}
var description *string
if roleDetails.Description != "" {
description = &roleDetails.Description
}
roles = append(roles, IdentityRole{
Key: roleDetails.Key,
Name: roleDetails.Name,
Description: description,
Permissions: permissions,
})
}
return ctx.JSON(http.StatusOK, IdentityResponse{
Roles: roles,
})
}
+308
View File
@@ -0,0 +1,308 @@
package queryapi_test
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
queryapi "queryorchestration/api/queryAPI"
"queryorchestration/internal/test"
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// mockScopeHandler returns an HTTP handler that mocks the Permit.io /v2/api-key/scope endpoint
func mockScopeHandler(t *testing.T) http.HandlerFunc {
return 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)
}
}
// TestGetIdentity_Success tests successful retrieval of user roles and permissions
func TestGetIdentity_Success(t *testing.T) {
// Create mock Permit.io server
roleAssignmentsHandler := func(w http.ResponseWriter, r *http.Request) {
// Return role assignments for the user
response := []map[string]interface{}{
{"role": "auditor", "user": "test-subject-id", "tenant": "default"},
{"role": "viewer", "user": "test-subject-id", "tenant": "default"},
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
err := json.NewEncoder(w).Encode(response)
require.NoError(t, err)
}
auditorRoleHandler := func(w http.ResponseWriter, r *http.Request) {
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)
}
viewerRoleHandler := func(w http.ResponseWriter, r *http.Request) {
response := map[string]interface{}{
"key": "viewer",
"name": "Viewer",
"description": "Basic viewing permissions",
"permissions": []string{"document:read"},
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
err := json.NewEncoder(w).Encode(response)
require.NoError(t, err)
}
// Create mock server with routing
mux := http.NewServeMux()
mux.HandleFunc("/v2/api-key/scope", mockScopeHandler(t))
mux.HandleFunc("/v2/facts/test-project/test-env/role_assignments", roleAssignmentsHandler)
mux.HandleFunc("/v2/schema/test-project/test-env/roles/auditor", auditorRoleHandler)
mux.HandleFunc("/v2/schema/test-project/test-env/roles/viewer", viewerRoleHandler)
server := httptest.NewServer(mux)
defer server.Close()
// Set environment variable for Permit.io base URL (project/env derived from API key scope)
t.Setenv("PERMIT_IO_BASE_URL", server.URL)
// Create controller config
cfg := &ControllerConfig{}
test.CreateDB(t, cfg)
initializeTestConfig(t, cfg)
cfg.SetPermitIOAPIKey("test-api-key")
svc := createControllerServices(cfg)
cons := queryapi.NewControllers(svc, cfg)
// Create echo context with user claims
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/identity", nil)
rec := httptest.NewRecorder()
ctx := e.NewContext(req, rec)
// Set user claims in context (simulating authenticated user)
ctx.Set("user_claims", map[string]interface{}{
"sub": "test-subject-id",
})
err := cons.GetIdentity(ctx)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, rec.Code)
var response queryapi.IdentityResponse
err = json.Unmarshal(rec.Body.Bytes(), &response)
require.NoError(t, err)
assert.Len(t, response.Roles, 2)
// Find auditor role
var auditorRole *queryapi.IdentityRole
var viewerRole *queryapi.IdentityRole
for i := range response.Roles {
if response.Roles[i].Key == "auditor" {
auditorRole = &response.Roles[i]
}
if response.Roles[i].Key == "viewer" {
viewerRole = &response.Roles[i]
}
}
require.NotNil(t, auditorRole)
assert.Equal(t, "Auditor", auditorRole.Name)
assert.NotNil(t, auditorRole.Description)
assert.Equal(t, "Read-only access to view documents", *auditorRole.Description)
assert.Equal(t, []string{"document:read", "document:list", "export:read"}, auditorRole.Permissions)
require.NotNil(t, viewerRole)
assert.Equal(t, "Viewer", viewerRole.Name)
assert.Equal(t, []string{"document:read"}, viewerRole.Permissions)
}
// TestGetIdentity_NoRoles tests user with no roles assigned
func TestGetIdentity_NoRoles(t *testing.T) {
// Create mock Permit.io server that returns empty roles
mux := http.NewServeMux()
mux.HandleFunc("/v2/api-key/scope", mockScopeHandler(t))
mux.HandleFunc("/v2/facts/test-project/test-env/role_assignments", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, err := w.Write([]byte(`[]`))
require.NoError(t, err)
})
server := httptest.NewServer(mux)
defer server.Close()
t.Setenv("PERMIT_IO_BASE_URL", server.URL)
cfg := &ControllerConfig{}
test.CreateDB(t, cfg)
initializeTestConfig(t, cfg)
cfg.SetPermitIOAPIKey("test-api-key")
svc := createControllerServices(cfg)
cons := queryapi.NewControllers(svc, cfg)
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/identity", nil)
rec := httptest.NewRecorder()
ctx := e.NewContext(req, rec)
ctx.Set("user_claims", map[string]interface{}{
"sub": "test-subject-id",
})
err := cons.GetIdentity(ctx)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, rec.Code)
var response queryapi.IdentityResponse
err = json.Unmarshal(rec.Body.Bytes(), &response)
require.NoError(t, err)
assert.Empty(t, response.Roles)
}
// TestGetIdentity_Unauthorized tests request without authentication
func TestGetIdentity_Unauthorized(t *testing.T) {
cfg := &ControllerConfig{}
test.CreateDB(t, cfg)
initializeTestConfig(t, cfg)
cfg.SetPermitIOAPIKey("test-api-key")
svc := createControllerServices(cfg)
cons := queryapi.NewControllers(svc, cfg)
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/identity", nil)
rec := httptest.NewRecorder()
ctx := e.NewContext(req, rec)
// No user_claims set - simulating unauthenticated request
err := cons.GetIdentity(ctx)
require.NoError(t, err)
assert.Equal(t, http.StatusUnauthorized, rec.Code)
var response queryapi.ErrorMessage
err = json.Unmarshal(rec.Body.Bytes(), &response)
require.NoError(t, err)
assert.Contains(t, response.Message, "Could not identify user")
}
// TestGetIdentity_PermitAPIError tests handling of Permit.io API errors
func TestGetIdentity_PermitAPIError(t *testing.T) {
// Create mock server that returns error
mux := http.NewServeMux()
mux.HandleFunc("/v2/api-key/scope", mockScopeHandler(t))
mux.HandleFunc("/v2/facts/test-project/test-env/role_assignments", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
_, err := w.Write([]byte(`{"error": "Internal server error"}`))
require.NoError(t, err)
})
server := httptest.NewServer(mux)
defer server.Close()
t.Setenv("PERMIT_IO_BASE_URL", server.URL)
cfg := &ControllerConfig{}
test.CreateDB(t, cfg)
initializeTestConfig(t, cfg)
cfg.SetPermitIOAPIKey("test-api-key")
svc := createControllerServices(cfg)
cons := queryapi.NewControllers(svc, cfg)
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/identity", nil)
rec := httptest.NewRecorder()
ctx := e.NewContext(req, rec)
ctx.Set("user_claims", map[string]interface{}{
"sub": "test-subject-id",
})
err := cons.GetIdentity(ctx)
require.NoError(t, err)
assert.Equal(t, http.StatusBadGateway, rec.Code)
var response queryapi.ErrorMessage
err = json.Unmarshal(rec.Body.Bytes(), &response)
require.NoError(t, err)
assert.Contains(t, response.Message, "Failed to retrieve roles")
}
// TestGetIdentity_RoleDetailsFallback tests graceful degradation when role details cannot be fetched
func TestGetIdentity_RoleDetailsFallback(t *testing.T) {
// Create mock server where role assignments work but role details fail
mux := http.NewServeMux()
mux.HandleFunc("/v2/api-key/scope", mockScopeHandler(t))
mux.HandleFunc("/v2/facts/test-project/test-env/role_assignments", func(w http.ResponseWriter, r *http.Request) {
response := []map[string]interface{}{
{"role": "auditor", "user": "test-subject-id", "tenant": "default"},
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
err := json.NewEncoder(w).Encode(response)
require.NoError(t, err)
})
mux.HandleFunc("/v2/schema/test-project/test-env/roles/auditor", func(w http.ResponseWriter, r *http.Request) {
// Role details endpoint returns error
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
_, err := w.Write([]byte(`{"error": "Role not found"}`))
require.NoError(t, err)
})
server := httptest.NewServer(mux)
defer server.Close()
t.Setenv("PERMIT_IO_BASE_URL", server.URL)
cfg := &ControllerConfig{}
test.CreateDB(t, cfg)
initializeTestConfig(t, cfg)
cfg.SetPermitIOAPIKey("test-api-key")
svc := createControllerServices(cfg)
cons := queryapi.NewControllers(svc, cfg)
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/identity", nil)
rec := httptest.NewRecorder()
ctx := e.NewContext(req, rec)
ctx.Set("user_claims", map[string]interface{}{
"sub": "test-subject-id",
})
err := cons.GetIdentity(ctx)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, rec.Code)
var response queryapi.IdentityResponse
err = json.Unmarshal(rec.Body.Bytes(), &response)
require.NoError(t, err)
// Should still return the role, but with fallback values
require.Len(t, response.Roles, 1)
assert.Equal(t, "auditor", response.Roles[0].Key)
assert.Equal(t, "auditor", response.Roles[0].Name) // Fallback to key
assert.Empty(t, response.Roles[0].Permissions) // Empty permissions on failure
}