package postgres import ( "context" "database/sql" "fmt" "time" "github.com/google/uuid" "github.com/kms/api-key-service/internal/domain" "github.com/kms/api-key-service/internal/repository" "github.com/lib/pq" ) // PermissionRepository implements the PermissionRepository interface for PostgreSQL type PermissionRepository struct { db repository.DatabaseProvider } // NewPermissionRepository creates a new PostgreSQL permission repository func NewPermissionRepository(db repository.DatabaseProvider) repository.PermissionRepository { return &PermissionRepository{db: db} } // CreateAvailablePermission creates a new available permission func (r *PermissionRepository) CreateAvailablePermission(ctx context.Context, permission *domain.AvailablePermission) error { query := ` INSERT INTO available_permissions ( id, scope, name, description, category, parent_scope, is_system, created_by, updated_by, created_at, updated_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) ` db := r.db.GetDB().(*sql.DB) now := time.Now() if permission.ID == uuid.Nil { permission.ID = uuid.New() } _, err := db.ExecContext(ctx, query, permission.ID, permission.Scope, permission.Name, permission.Description, permission.Category, permission.ParentScope, permission.IsSystem, permission.CreatedBy, permission.UpdatedBy, now, now, ) if err != nil { return fmt.Errorf("failed to create available permission: %w", err) } permission.CreatedAt = now permission.UpdatedAt = now return nil } // GetAvailablePermission retrieves an available permission by ID func (r *PermissionRepository) GetAvailablePermission(ctx context.Context, permissionID uuid.UUID) (*domain.AvailablePermission, error) { query := ` SELECT id, scope, name, description, category, parent_scope, is_system, created_at, created_by, updated_at, updated_by FROM available_permissions WHERE id = $1 ` db := r.db.GetDB().(*sql.DB) row := db.QueryRowContext(ctx, query, permissionID) permission := &domain.AvailablePermission{} err := row.Scan( &permission.ID, &permission.Scope, &permission.Name, &permission.Description, &permission.Category, &permission.ParentScope, &permission.IsSystem, &permission.CreatedAt, &permission.CreatedBy, &permission.UpdatedAt, &permission.UpdatedBy, ) if err != nil { if err == sql.ErrNoRows { return nil, fmt.Errorf("permission with ID '%s' not found", permissionID) } return nil, fmt.Errorf("failed to get available permission: %w", err) } return permission, nil } // GetAvailablePermissionByScope retrieves an available permission by scope func (r *PermissionRepository) GetAvailablePermissionByScope(ctx context.Context, scope string) (*domain.AvailablePermission, error) { query := ` SELECT id, scope, name, description, category, parent_scope, is_system, created_at, created_by, updated_at, updated_by FROM available_permissions WHERE scope = $1 ` db := r.db.GetDB().(*sql.DB) row := db.QueryRowContext(ctx, query, scope) permission := &domain.AvailablePermission{} err := row.Scan( &permission.ID, &permission.Scope, &permission.Name, &permission.Description, &permission.Category, &permission.ParentScope, &permission.IsSystem, &permission.CreatedAt, &permission.CreatedBy, &permission.UpdatedAt, &permission.UpdatedBy, ) if err != nil { if err == sql.ErrNoRows { return nil, fmt.Errorf("permission with scope '%s' not found", scope) } return nil, fmt.Errorf("failed to get available permission by scope: %w", err) } return permission, nil } // ListAvailablePermissions retrieves available permissions with pagination and filtering func (r *PermissionRepository) ListAvailablePermissions(ctx context.Context, category string, includeSystem bool, limit, offset int) ([]*domain.AvailablePermission, error) { var args []interface{} var whereClauses []string argIndex := 1 // Build WHERE clause based on filters if category != "" { whereClauses = append(whereClauses, fmt.Sprintf("category = $%d", argIndex)) args = append(args, category) argIndex++ } if !includeSystem { whereClauses = append(whereClauses, fmt.Sprintf("is_system = $%d", argIndex)) args = append(args, false) argIndex++ } whereClause := "" if len(whereClauses) > 0 { whereClause = "WHERE " + fmt.Sprintf("%s", whereClauses[0]) for i := 1; i < len(whereClauses); i++ { whereClause += " AND " + whereClauses[i] } } query := fmt.Sprintf(` SELECT id, scope, name, description, category, parent_scope, is_system, created_at, created_by, updated_at, updated_by FROM available_permissions %s ORDER BY category, scope LIMIT $%d OFFSET $%d `, whereClause, argIndex, argIndex+1) args = append(args, limit, offset) db := r.db.GetDB().(*sql.DB) rows, err := db.QueryContext(ctx, query, args...) if err != nil { return nil, fmt.Errorf("failed to list available permissions: %w", err) } defer rows.Close() var permissions []*domain.AvailablePermission for rows.Next() { permission := &domain.AvailablePermission{} err := rows.Scan( &permission.ID, &permission.Scope, &permission.Name, &permission.Description, &permission.Category, &permission.ParentScope, &permission.IsSystem, &permission.CreatedAt, &permission.CreatedBy, &permission.UpdatedAt, &permission.UpdatedBy, ) if err != nil { return nil, fmt.Errorf("failed to scan available permission: %w", err) } permissions = append(permissions, permission) } if err = rows.Err(); err != nil { return nil, fmt.Errorf("failed to iterate available permissions: %w", err) } return permissions, nil } // UpdateAvailablePermission updates an available permission func (r *PermissionRepository) UpdateAvailablePermission(ctx context.Context, permissionID uuid.UUID, permission *domain.AvailablePermission) error { query := ` UPDATE available_permissions SET scope = $2, name = $3, description = $4, category = $5, parent_scope = $6, is_system = $7, updated_by = $8, updated_at = $9 WHERE id = $1 ` db := r.db.GetDB().(*sql.DB) now := time.Now() result, err := db.ExecContext(ctx, query, permissionID, permission.Scope, permission.Name, permission.Description, permission.Category, permission.ParentScope, permission.IsSystem, permission.UpdatedBy, now, ) if err != nil { return fmt.Errorf("failed to update available permission: %w", err) } rowsAffected, err := result.RowsAffected() if err != nil { return fmt.Errorf("failed to get rows affected: %w", err) } if rowsAffected == 0 { return fmt.Errorf("permission with ID %s not found", permissionID) } permission.UpdatedAt = now return nil } // DeleteAvailablePermission deletes an available permission func (r *PermissionRepository) DeleteAvailablePermission(ctx context.Context, permissionID uuid.UUID) error { // First check if the permission has any child permissions checkChildrenQuery := ` SELECT COUNT(*) FROM available_permissions WHERE parent_scope = (SELECT scope FROM available_permissions WHERE id = $1) ` db := r.db.GetDB().(*sql.DB) var childCount int err := db.QueryRowContext(ctx, checkChildrenQuery, permissionID).Scan(&childCount) if err != nil { return fmt.Errorf("failed to check for child permissions: %w", err) } if childCount > 0 { return fmt.Errorf("cannot delete permission: it has %d child permissions", childCount) } // Check if the permission is granted to any tokens checkGrantsQuery := ` SELECT COUNT(*) FROM granted_permissions WHERE permission_id = $1 AND revoked = false ` var grantCount int err = db.QueryRowContext(ctx, checkGrantsQuery, permissionID).Scan(&grantCount) if err != nil { return fmt.Errorf("failed to check for active grants: %w", err) } if grantCount > 0 { return fmt.Errorf("cannot delete permission: it is currently granted to %d tokens", grantCount) } // Delete the permission deleteQuery := `DELETE FROM available_permissions WHERE id = $1` result, err := db.ExecContext(ctx, deleteQuery, permissionID) if err != nil { return fmt.Errorf("failed to delete available permission: %w", err) } rowsAffected, err := result.RowsAffected() if err != nil { return fmt.Errorf("failed to get rows affected: %w", err) } if rowsAffected == 0 { return fmt.Errorf("permission with ID %s not found", permissionID) } return nil } // ValidatePermissionScopes checks if all given scopes exist and are valid func (r *PermissionRepository) ValidatePermissionScopes(ctx context.Context, scopes []string) ([]string, error) { if len(scopes) == 0 { return []string{}, nil } query := ` SELECT scope FROM available_permissions WHERE scope = ANY($1) ` db := r.db.GetDB().(*sql.DB) rows, err := db.QueryContext(ctx, query, pq.Array(scopes)) if err != nil { return nil, fmt.Errorf("failed to validate permission scopes: %w", err) } defer rows.Close() validScopes := make(map[string]bool) for rows.Next() { var scope string if err := rows.Scan(&scope); err != nil { return nil, fmt.Errorf("failed to scan scope: %w", err) } validScopes[scope] = true } if err = rows.Err(); err != nil { return nil, fmt.Errorf("error iterating scopes: %w", err) } var result []string for _, scope := range scopes { if validScopes[scope] { result = append(result, scope) } } return result, nil } // GetPermissionHierarchy returns all parent and child permissions for given scopes func (r *PermissionRepository) GetPermissionHierarchy(ctx context.Context, scopes []string) ([]*domain.AvailablePermission, error) { if len(scopes) == 0 { return []*domain.AvailablePermission{}, nil } // Use recursive CTE to get full hierarchy query := ` WITH RECURSIVE permission_hierarchy AS ( -- Base case: get permissions matching the input scopes SELECT id, scope, name, description, category, parent_scope, is_system, created_at, created_by, updated_at, updated_by, 0 as level FROM available_permissions WHERE scope = ANY($1) UNION ALL -- Recursive case: get all parents and children SELECT ap.id, ap.scope, ap.name, ap.description, ap.category, ap.parent_scope, ap.is_system, ap.created_at, ap.created_by, ap.updated_at, ap.updated_by, ph.level + 1 as level FROM available_permissions ap JOIN permission_hierarchy ph ON ( -- Get parents (where ap.scope = ph.parent_scope) ap.scope = ph.parent_scope OR -- Get children (where ap.parent_scope = ph.scope) ap.parent_scope = ph.scope ) WHERE ph.level < 5 -- Prevent infinite recursion ) SELECT DISTINCT id, scope, name, description, category, parent_scope, is_system, created_at, created_by, updated_at, updated_by FROM permission_hierarchy ORDER BY scope ` db := r.db.GetDB().(*sql.DB) rows, err := db.QueryContext(ctx, query, pq.Array(scopes)) if err != nil { return nil, fmt.Errorf("failed to get permission hierarchy: %w", err) } defer rows.Close() var permissions []*domain.AvailablePermission for rows.Next() { permission := &domain.AvailablePermission{} err := rows.Scan( &permission.ID, &permission.Scope, &permission.Name, &permission.Description, &permission.Category, &permission.ParentScope, &permission.IsSystem, &permission.CreatedAt, &permission.CreatedBy, &permission.UpdatedAt, &permission.UpdatedBy, ) if err != nil { return nil, fmt.Errorf("failed to scan permission hierarchy: %w", err) } permissions = append(permissions, permission) } if err = rows.Err(); err != nil { return nil, fmt.Errorf("failed to iterate permission hierarchy: %w", err) } return permissions, nil } // GrantedPermissionRepository implements the GrantedPermissionRepository interface for PostgreSQL type GrantedPermissionRepository struct { db repository.DatabaseProvider } // NewGrantedPermissionRepository creates a new PostgreSQL granted permission repository func NewGrantedPermissionRepository(db repository.DatabaseProvider) repository.GrantedPermissionRepository { return &GrantedPermissionRepository{db: db} } // GrantPermissions grants multiple permissions to a token func (r *GrantedPermissionRepository) GrantPermissions(ctx context.Context, grants []*domain.GrantedPermission) error { if len(grants) == 0 { return nil } db := r.db.GetDB().(*sql.DB) tx, err := db.BeginTx(ctx, nil) if err != nil { return fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback() query := ` INSERT INTO granted_permissions ( id, token_type, token_id, permission_id, scope, created_by, created_at ) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (token_type, token_id, permission_id) DO NOTHING ` stmt, err := tx.PrepareContext(ctx, query) if err != nil { return fmt.Errorf("failed to prepare statement: %w", err) } defer stmt.Close() now := time.Now() for _, grant := range grants { if grant.ID == uuid.Nil { grant.ID = uuid.New() } _, err = stmt.ExecContext(ctx, grant.ID, string(grant.TokenType), grant.TokenID, grant.PermissionID, grant.Scope, grant.CreatedBy, now, ) if err != nil { return fmt.Errorf("failed to grant permission: %w", err) } grant.CreatedAt = now } if err = tx.Commit(); err != nil { return fmt.Errorf("failed to commit transaction: %w", err) } return nil } // GetGrantedPermissions retrieves all granted permissions for a token func (r *GrantedPermissionRepository) GetGrantedPermissions(ctx context.Context, tokenType domain.TokenType, tokenID uuid.UUID) ([]*domain.GrantedPermission, error) { query := ` SELECT id, token_type, token_id, permission_id, scope, created_at, created_by, revoked FROM granted_permissions WHERE token_type = $1 AND token_id = $2 AND revoked = false ORDER BY created_at ASC ` db := r.db.GetDB().(*sql.DB) rows, err := db.QueryContext(ctx, query, string(tokenType), tokenID) if err != nil { return nil, fmt.Errorf("failed to query granted permissions: %w", err) } defer rows.Close() var permissions []*domain.GrantedPermission for rows.Next() { perm := &domain.GrantedPermission{} var tokenTypeStr string err := rows.Scan( &perm.ID, &tokenTypeStr, &perm.TokenID, &perm.PermissionID, &perm.Scope, &perm.CreatedAt, &perm.CreatedBy, &perm.Revoked, ) if err != nil { return nil, fmt.Errorf("failed to scan granted permission: %w", err) } perm.TokenType = domain.TokenType(tokenTypeStr) permissions = append(permissions, perm) } if err = rows.Err(); err != nil { return nil, fmt.Errorf("error iterating granted permissions: %w", err) } return permissions, nil } // GetGrantedPermissionScopes retrieves only the scopes for a token (more efficient) func (r *GrantedPermissionRepository) GetGrantedPermissionScopes(ctx context.Context, tokenType domain.TokenType, tokenID uuid.UUID) ([]string, error) { query := ` SELECT scope FROM granted_permissions WHERE token_type = $1 AND token_id = $2 AND revoked = false ORDER BY scope ASC ` db := r.db.GetDB().(*sql.DB) rows, err := db.QueryContext(ctx, query, string(tokenType), tokenID) if err != nil { return nil, fmt.Errorf("failed to query granted permission scopes: %w", err) } defer rows.Close() var scopes []string for rows.Next() { var scope string if err := rows.Scan(&scope); err != nil { return nil, fmt.Errorf("failed to scan permission scope: %w", err) } scopes = append(scopes, scope) } if err = rows.Err(); err != nil { return nil, fmt.Errorf("error iterating permission scopes: %w", err) } return scopes, nil } // RevokePermission revokes a specific permission from a token func (r *GrantedPermissionRepository) RevokePermission(ctx context.Context, grantID uuid.UUID, revokedBy string) error { query := ` UPDATE granted_permissions SET revoked = true, revoked_by = $2, revoked_at = $3 WHERE id = $1 AND revoked = false ` db := r.db.GetDB().(*sql.DB) now := time.Now() result, err := db.ExecContext(ctx, query, grantID, revokedBy, now) if err != nil { return fmt.Errorf("failed to revoke permission: %w", err) } rowsAffected, err := result.RowsAffected() if err != nil { return fmt.Errorf("failed to get rows affected: %w", err) } if rowsAffected == 0 { return fmt.Errorf("permission grant with ID %s not found or already revoked", grantID) } return nil } // RevokeAllPermissions revokes all permissions from a token func (r *GrantedPermissionRepository) RevokeAllPermissions(ctx context.Context, tokenType domain.TokenType, tokenID uuid.UUID, revokedBy string) error { query := ` UPDATE granted_permissions SET revoked = true, revoked_by = $3, revoked_at = $4 WHERE token_type = $1 AND token_id = $2 AND revoked = false ` db := r.db.GetDB().(*sql.DB) now := time.Now() result, err := db.ExecContext(ctx, query, tokenType, tokenID, revokedBy, now) if err != nil { return fmt.Errorf("failed to revoke all permissions: %w", err) } rowsAffected, err := result.RowsAffected() if err != nil { return fmt.Errorf("failed to get rows affected: %w", err) } // Note: rowsAffected being 0 is not necessarily an error here - // the token might not have had any active permissions _ = rowsAffected return nil } // HasPermission checks if a token has a specific permission func (r *GrantedPermissionRepository) HasPermission(ctx context.Context, tokenType domain.TokenType, tokenID uuid.UUID, scope string) (bool, error) { query := ` SELECT 1 FROM granted_permissions gp JOIN available_permissions ap ON gp.permission_id = ap.id WHERE gp.token_type = $1 AND gp.token_id = $2 AND gp.scope = $3 AND gp.revoked = false LIMIT 1 ` db := r.db.GetDB().(*sql.DB) var exists int err := db.QueryRowContext(ctx, query, string(tokenType), tokenID, scope).Scan(&exists) if err != nil { if err == sql.ErrNoRows { return false, nil } return false, fmt.Errorf("failed to check permission: %w", err) } return true, nil } // HasAnyPermission checks if a token has any of the specified permissions func (r *GrantedPermissionRepository) HasAnyPermission(ctx context.Context, tokenType domain.TokenType, tokenID uuid.UUID, scopes []string) (map[string]bool, error) { if len(scopes) == 0 { return make(map[string]bool), nil } query := ` SELECT gp.scope FROM granted_permissions gp JOIN available_permissions ap ON gp.permission_id = ap.id WHERE gp.token_type = $1 AND gp.token_id = $2 AND gp.scope = ANY($3) AND gp.revoked = false ` db := r.db.GetDB().(*sql.DB) rows, err := db.QueryContext(ctx, query, string(tokenType), tokenID, pq.Array(scopes)) if err != nil { return nil, fmt.Errorf("failed to check permissions: %w", err) } defer rows.Close() result := make(map[string]bool) // Initialize all scopes as false for _, scope := range scopes { result[scope] = false } // Mark found permissions as true for rows.Next() { var scope string if err := rows.Scan(&scope); err != nil { return nil, fmt.Errorf("failed to scan permission scope: %w", err) } result[scope] = true } if err = rows.Err(); err != nil { return nil, fmt.Errorf("error iterating permission results: %w", err) } return result, nil }