Skip to content

Commit 2db563c

Browse files
committed
Add support for Okta SSO via generic OIDC auth
Integrate OIDC auth into platform python tests Signed-off-by: Karakatiza666 <bulakh.96@gmail.com>
1 parent 7de8a41 commit 2db563c

File tree

42 files changed

+2498
-698
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+2498
-698
lines changed

.github/workflows/test-integration-platform.yml

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,14 @@ jobs:
5959
if: ${{ !contains(vars.CI_SKIP_JOBS, 'manager-https') }}
6060
name: Make sure manager runs with HTTPS
6161
runs-on: ubuntu-latest-amd64
62+
63+
# Environment variables for OIDC authentication (if available)
64+
env:
65+
OIDC_TEST_ISSUER: ${{ vars.OIDC_TEST_ISSUER }}
66+
OIDC_TEST_CLIENT_ID: ${{ vars.OIDC_TEST_CLIENT_ID }}
67+
OIDC_TEST_CLIENT_SECRET: ${{ secrets.OIDC_TEST_CLIENT_SECRET }}
68+
OIDC_TEST_USERNAME: ${{ secrets.OIDC_TEST_USERNAME }}
69+
OIDC_TEST_PASSWORD: ${{ secrets.OIDC_TEST_PASSWORD }}
6270
steps:
6371
- name: Checkout repository
6472
uses: actions/checkout@v4
@@ -85,6 +93,11 @@ jobs:
8593
--health-retries=5 \
8694
--mount type=bind,src=./test-tls,dst=/home/ubuntu/test-tls,readonly \
8795
-p 8080:8080 \
96+
-e AUTH_PROVIDER="${{ vars.OIDC_TEST_ISSUER && 'generic-oidc' || 'none' }}" \
97+
-e AUTH_CLIENT_ID="${{ vars.OIDC_TEST_CLIENT_ID }}" \
98+
-e AUTH_ISSUER="${{ vars.OIDC_TEST_ISSUER }}" \
99+
-e RUST_LOG=info \
100+
-e RUST_BACKTRACE=1 \
88101
${{ vars.FELDERA_IMAGE_NAME }}:sha-${{ github.sha }} \
89102
--enable-https \
90103
--https-tls-cert-path /home/ubuntu/test-tls/tls_test.crt \
@@ -115,6 +128,12 @@ jobs:
115128
FELDERA_HOST: https://localhost:8080
116129
PYTHONPATH: ${{ github.workspace }}/python
117130
IN_CI: 1 # We use this flag to skip some kafka tests in the python code base
131+
# OIDC environment variables for authentication
132+
OIDC_TEST_ISSUER: ${{ vars.OIDC_TEST_ISSUER }}
133+
OIDC_TEST_CLIENT_ID: ${{ vars.OIDC_TEST_CLIENT_ID }}
134+
OIDC_TEST_CLIENT_SECRET: ${{ secrets.OIDC_TEST_CLIENT_SECRET }}
135+
OIDC_TEST_USERNAME: ${{ secrets.OIDC_TEST_USERNAME }}
136+
OIDC_TEST_PASSWORD: ${{ secrets.OIDC_TEST_PASSWORD }}
118137
- name: Logs & Cleanup
119138
if: always()
120139
run: |
@@ -137,11 +156,26 @@ jobs:
137156
# target: aarch64-unknown-linux-gnu
138157
runs-on: ${{ matrix.runner }}
139158

159+
# Environment variables for OIDC authentication (if available)
160+
env:
161+
OIDC_TEST_ISSUER: ${{ vars.OIDC_TEST_ISSUER }}
162+
OIDC_TEST_CLIENT_ID: ${{ vars.OIDC_TEST_CLIENT_ID }}
163+
OIDC_TEST_CLIENT_SECRET: ${{ secrets.OIDC_TEST_CLIENT_SECRET }}
164+
OIDC_TEST_USERNAME: ${{ secrets.OIDC_TEST_USERNAME }}
165+
OIDC_TEST_PASSWORD: ${{ secrets.OIDC_TEST_PASSWORD }}
166+
140167
container:
141168
image: ghcr.io/feldera/feldera-dev:sha-9a54a381bb958ba78da125976a2f7731db3d7a4f
142169
services:
143170
pipeline-manager:
144171
image: ${{ vars.FELDERA_IMAGE_NAME }}:sha-${{ github.sha }}
172+
env:
173+
# Configure OIDC authentication if available, otherwise use no auth
174+
AUTH_PROVIDER: ${{ vars.OIDC_TEST_ISSUER && 'generic-oidc' || 'none' }}
175+
AUTH_CLIENT_ID: ${{ vars.OIDC_TEST_CLIENT_ID }}
176+
AUTH_ISSUER: ${{ vars.OIDC_TEST_ISSUER }}
177+
RUST_LOG: info
178+
RUST_BACKTRACE: 1
145179
options: >-
146180
--health-cmd "curl --fail --request GET --url http://localhost:8080/healthz || exit 1"
147181
--health-interval 10s
@@ -152,6 +186,16 @@ jobs:
152186
- name: Checkout repository
153187
uses: actions/checkout@v4
154188

189+
- name: Check OIDC configuration and connectivity
190+
if: vars.OIDC_TEST_ISSUER != '' && vars.OIDC_TEST_CLIENT_ID != ''
191+
run: |
192+
echo "OIDC configuration detected, verifying connectivity..."
193+
echo "Testing OIDC discovery endpoint: ${{ vars.OIDC_TEST_ISSUER }}"
194+
ISSUER_URL="${{ vars.OIDC_TEST_ISSUER }}"
195+
ISSUER_URL="${ISSUER_URL%/}" # Strip trailing slash
196+
curl -f -s "${ISSUER_URL}/.well-known/openid-configuration" > /dev/null
197+
echo "OIDC discovery endpoint is accessible"
198+
155199
- name: Validate and run packaged demos
156200
if: ${{ vars.CI_DRY_RUN != 'true' }}
157201
run: |
@@ -170,3 +214,9 @@ jobs:
170214
PYTHONPATH: ${{ github.workspace }}/python
171215
IN_CI: 1 # We use this flag to skip some kafka tests in the python code base
172216
FELDERA_HTTPS_TLS_CERT: /home/ubuntu/test-tls/tls_test.crt
217+
# OIDC environment variables for authentication
218+
OIDC_TEST_ISSUER: ${{ vars.OIDC_TEST_ISSUER }}
219+
OIDC_TEST_CLIENT_ID: ${{ vars.OIDC_TEST_CLIENT_ID }}
220+
OIDC_TEST_CLIENT_SECRET: ${{ secrets.OIDC_TEST_CLIENT_SECRET }}
221+
OIDC_TEST_USERNAME: ${{ secrets.OIDC_TEST_USERNAME }}
222+
OIDC_TEST_PASSWORD: ${{ secrets.OIDC_TEST_PASSWORD }}

CLAUDE.md

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6652,6 +6652,10 @@ POST /api/pipelines - Create pipeline
66526652
GET /api/pipelines/{id} - Get pipeline details
66536653
PATCH /api/pipelines/{id} - Update pipeline
66546654
DELETE /api/pipelines/{id} - Delete pipeline
6655+
6656+
GET /v0/config - Get configuration (includes tenant info)
6657+
GET /config/authentication - Get authentication provider configuration
6658+
GET /config/demos - Get list of available demos
66556659
```
66566660
66576661
#### Pipeline Lifecycle
@@ -6673,10 +6677,53 @@ GET /api/pipelines/{id}/stats - Get runtime statistics
66736677
66746678
### Authentication System
66756679
6676-
- **JWT Tokens**: Stateless authentication
6677-
- **API Keys**: Service-to-service authentication
6678-
- **Multi-tenant**: Tenant isolation and resource management
6679-
- **RBAC**: Role-based access control
6680+
The pipeline-manager supports multiple authentication providers through a unified OIDC/OAuth2 framework:
6681+
6682+
#### **Supported Providers**
6683+
- **None**: No authentication (development/testing)
6684+
- **AWS Cognito**
6685+
- **Generic OIDC** - Okta
6686+
6687+
#### **Authentication Mechanisms**
6688+
- **OIDC Tokens**: OIDC-compliant Access token validation with RS256 signature verification
6689+
- **API Keys**: User-generated keys that
6690+
6691+
Authentication of HTTP API requests is performed through Authorization header via `Bearer <token>` value
6692+
6693+
Additional features:
6694+
- JWK Caching: Automatic public key fetching and caching from provider endpoints
6695+
- Bearer Token Authorization: Client sends Access OIDC token as Bearer token; all claims (tenant, groups, etc.) are extracted from this Access token
6696+
6697+
#### **Configuration**
6698+
```bash
6699+
# Environment variables for OIDC providers
6700+
AUTH_ISSUER=https://your-domain.okta.com/oauth2/<custom-auth-server-id>
6701+
AUTH_CLIENT_ID=your-client-id
6702+
6703+
# For AWS Cognito (additional variables)
6704+
AWS_COGNITO_LOGIN_URL=https://your-domain.auth.region.amazoncognito.com/login
6705+
AWS_COGNITO_LOGOUT_URL=https://your-domain.auth.region.amazoncognito.com/logout
6706+
```
6707+
6708+
#### **Authorization mechanisms**
6709+
6710+
The authentication system supports flexible tenant assignment strategies across all supported OIDC providers (AWS Cognito, Okta):
6711+
6712+
**Tenant Assignment Strategies:**
6713+
- `--individual-tenant` (default: true) - Creates individual tenants based on user's `sub` claim
6714+
- `--issuer-tenant` (default: false) - Derives tenant name from auth issuer hostname (e.g., `company.okta.com`)
6715+
- Custom `tenant` claim - enabled by default - If present in OIDC Access token is used to directly determine the authorized tenant
6716+
6717+
**Tenant Resolution Priority (all providers):**
6718+
1. `tenant` claim (explicit tenant assignment via OIDC provider)
6719+
2. Issuer domain extraction (when `--issuer-tenant` enabled)
6720+
3. User `sub` claim (when `--individual-tenant` enabled)
6721+
6722+
**Group-based authorization**
6723+
If --authorized_groups is configured the user has to have at least one of these groups in `groups` claim of OIDC Access Token
6724+
6725+
#### **Enterprise Features**
6726+
- **Fault tolerance**: Mechanism to recover from a crash by making periodic checkpoints, identifying and replaying lost state
66806727

66816728
## Development Workflow
66826729

crates/pipeline-manager/src/api/endpoints/config.rs

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
// Configuration API to retrieve the current authentication configuration and list of demos
2-
use actix_web::{get, web::Data as WebData, HttpRequest, HttpResponse};
2+
use actix_web::{
3+
get,
4+
web::{Data as WebData, ReqData},
5+
HttpRequest, HttpResponse,
6+
};
37
use feldera_types::license::DisplaySchedule;
48
use serde::Serialize;
59
use utoipa::ToSchema;
610

711
use crate::api::main::ServerState;
12+
use crate::db::storage::Storage;
13+
use crate::db::types::tenant::TenantId;
814
use crate::error::ManagerError;
915
use crate::license::{LicenseCheck, LicenseValidity};
1016
use crate::unstable_features;
@@ -140,7 +146,8 @@ impl Configuration {
140146
#[get("/config")]
141147
pub(crate) async fn get_config(
142148
state: WebData<ServerState>,
143-
_req: HttpRequest,
149+
_client: WebData<awc::Client>,
150+
_tenant_id: ReqData<TenantId>,
144151
) -> Result<HttpResponse, ManagerError> {
145152
let config = Configuration::gather(&state).await;
146153
Ok(HttpResponse::Ok().json(config))
@@ -195,5 +202,52 @@ pub(crate) async fn get_config_demos(
195202
Ok(HttpResponse::Ok().json(&state.demos))
196203
}
197204

205+
#[derive(Serialize, ToSchema)]
206+
pub(crate) struct SessionInfo {
207+
/// Current user's tenant ID
208+
pub tenant_id: TenantId,
209+
/// Current user's tenant name
210+
pub tenant_name: String,
211+
}
212+
213+
impl SessionInfo {
214+
pub(crate) async fn gather(state: &ServerState, tenant_id: TenantId) -> Self {
215+
let db = state.db.lock().await;
216+
let tenant_name = db
217+
.get_tenant_name(tenant_id)
218+
.await
219+
.unwrap_or_else(|_| "unknown".to_string());
220+
221+
SessionInfo {
222+
tenant_id,
223+
tenant_name,
224+
}
225+
}
226+
}
227+
228+
/// Retrieve current session information.
229+
#[utoipa::path(
230+
context_path = "/v0",
231+
security(("JSON web token (JWT) or API key" = [])),
232+
responses(
233+
(status = OK
234+
, description = "The response body contains current session information including tenant details."
235+
, content_type = "application/json"
236+
, body = SessionInfo),
237+
(status = INTERNAL_SERVER_ERROR
238+
, description = "Request failed."
239+
, body = ErrorResponse),
240+
),
241+
tag = "Configuration"
242+
)]
243+
#[get("/config/session")]
244+
pub(crate) async fn get_config_session(
245+
state: WebData<ServerState>,
246+
tenant_id: ReqData<TenantId>,
247+
) -> Result<HttpResponse, ManagerError> {
248+
let session_info = SessionInfo::gather(&state, *tenant_id).await;
249+
Ok(HttpResponse::Ok().json(session_info))
250+
}
251+
198252
#[derive(Serialize, ToSchema)]
199253
pub(crate) struct EmptyResponse {}

crates/pipeline-manager/src/api/main.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ It contains the following fields:
218218
// Configuration
219219
endpoints::config::get_config_authentication,
220220
endpoints::config::get_config_demos,
221+
endpoints::config::get_config_session,
221222
endpoints::config::get_config,
222223
223224
// Metrics
@@ -230,14 +231,18 @@ It contains the following fields:
230231
// Authentication
231232
crate::auth::AuthProvider,
232233
crate::auth::ProviderAwsCognito,
233-
crate::auth::ProviderGoogleIdentity,
234+
crate::auth::ProviderGenericOidc,
234235
235236
// Common
236237
crate::db::types::version::Version,
238+
crate::db::types::tenant::TenantId,
239+
crate::license::DisplaySchedule,
240+
crate::license::LicenseInformation,
237241
crate::license::LicenseValidity,
238242
crate::api::endpoints::config::UpdateInformation,
239243
crate::api::endpoints::config::Configuration,
240244
crate::api::endpoints::config::BuildInformation,
245+
crate::api::endpoints::config::SessionInfo,
241246
242247
// Pipeline
243248
crate::db::types::pipeline::PipelineId,
@@ -490,6 +495,7 @@ fn api_scope() -> Scope {
490495
// Configuration endpoints
491496
.service(endpoints::config::get_config)
492497
.service(endpoints::config::get_config_demos)
498+
.service(endpoints::config::get_config_session)
493499
// Metrics of all pipelines belonging to this tenant
494500
.service(endpoints::metrics::get_metrics)
495501
// Cluster health check
@@ -643,7 +649,18 @@ pub async fn run(
643649
let auth_configuration = match api_config.auth_provider {
644650
crate::config::AuthProviderType::None => None,
645651
crate::config::AuthProviderType::AwsCognito => Some(crate::auth::aws_auth_config()),
646-
crate::config::AuthProviderType::GoogleIdentity => Some(crate::auth::google_auth_config()),
652+
crate::config::AuthProviderType::GenericOidc => {
653+
match crate::auth::generic_oidc_auth_config(&api_config).await {
654+
Ok(config) => Some(config),
655+
Err(e) => {
656+
error!("Failed to configure generic OIDC authentication: {}", e);
657+
return Err(anyhow::anyhow!(
658+
"Authentication configuration failed: {}",
659+
e
660+
));
661+
}
662+
}
663+
}
647664
};
648665
let server = match auth_configuration {
649666
// We instantiate an awc::Client that can be used if the api-server needs to

0 commit comments

Comments
 (0)