Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 28 additions & 78 deletions docs/enterprise/setting-up-okta.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ This guide walks you through configuring Okta as your identity provider for Bifr
- An Okta organization with admin access
- Bifrost Enterprise deployed and accessible
- The redirect URI for your Bifrost instance (e.g., `https://your-bifrost-domain.com/login`)

- Ensure you have created all the [roles in Bifrost](/enterprise/rbac) that you are aiming to map to with Okta.
---

## Step 1: Create an OIDC Application
Expand Down Expand Up @@ -71,39 +71,12 @@ Configure the following settings for your application:

---

## Step 3: Configure Authorization Server (optional)
## Step 3: Create Custom Role Attribute

<Note>
The default authorization server (`/oauth2/default`) is available to all Okta plans and **supports custom claims**, including role claims. The API Access Management paid add-on is only required to create additional custom authorization servers beyond the default.
You can use both roles and/or groups for assigning roles to users. You can learn more about [RBAC](/enterprise/rbac) docs. Roles takes precedence over groups in role assignment.
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</Note>

Bifrost uses Okta's Authorization Server to issue tokens. You have three options:

1. **Use `/oauth2/default` with role claims (recommended)** — Complete Steps 4-7 to configure custom role claims on the default authorization server. This enables automatic RBAC synchronization.

2. **Use `/oauth2/default` without role claims** — Skip Steps 4-7. The first user to sign in automatically receives the Admin role and can manage RBAC for all subsequent users through the Bifrost dashboard.

3. **Skip Step 3 entirely** — Authorization is not configured through Okta. You'll need an alternative authentication mechanism.

### Configuring the Authorization Server

1. Navigate to **Security** → **API**
2. Click on **Authorization Servers**

<Frame>
<img src="/media/user-provisioning/okta-authorization-server.png" alt="Okta Authorization Servers" />
</Frame>

3. Note the **Issuer URI** for your authorization server (e.g., `https://your-domain.okta.com/oauth2/default`)

<Note>
The Issuer URI is used as the `issuerUrl` in your Bifrost configuration. Make sure to use the full URL including `/oauth2/default` (or your custom authorization server path).
</Note>

---

## Step 4: Create Custom Role Attribute

To map Okta users to Bifrost roles (Admin, Developer, Viewer), you need to create a custom attribute.

1. Navigate to **Directory** → **Profile Editor**
Expand Down Expand Up @@ -133,7 +106,7 @@ To map Okta users to Bifrost roles (Admin, Developer, Viewer), you need to creat

---

## Step 5: Add Role Claim to Tokens
## Step 4: Add Role Claim to Tokens

Configure the authorization server to include the role in the access token.

Expand Down Expand Up @@ -164,11 +137,11 @@ If you named your custom attribute differently, update the Value expression acco

---

## Step 6: Configure Groups for Team and Role Synchronization
## Step 5: Configure Groups

Bifrost can automatically sync Okta groups for two purposes:
- **Team synchronization** — Groups are synced as Bifrost teams
- **Role mapping** — Groups can be mapped to Bifrost roles (Admin, Developer, Viewer) using Group-to-Role Mappings in the Bifrost UI
- **Role mapping** — Groups can be mapped to Bifrost roles (Admin, Developer, Viewer) using Group-to-Role Mappings in the Bifrost UI.

### Create Groups in Okta

Expand All @@ -191,31 +164,6 @@ Use a consistent naming convention for your groups. This makes it easier to conf

### Add Groups Claim to Tokens

You have two options for configuring the groups claim. Choose the one that best fits your Okta plan and requirements.

#### Option A: Using App-Level Groups Claim (All Okta Plans)

This approach configures the groups claim directly in your application's settings and works with all Okta plans, including free tiers.

1. Navigate to your application's **Sign On** tab
2. Scroll down to the **OpenID Connect ID Token** section
3. Click **Edit** to modify the settings
4. Configure the **Groups claim filter**:
- **Groups claim type**: Filter
- **Groups claim filter**: Set a claim name (e.g., `groups`) and filter condition (e.g., "Starts with" `bifrost-staging`)

<Frame>
<img src="/media/user-provisioning/okta-app-group-claim-setup.png" alt="Application Groups claim configuration" />
</Frame>

5. Click **Save**

<Note>
The filter ensures only relevant groups are included in the token. Adjust the filter condition based on your group naming convention.
</Note>

#### Option B: Using Authorization Server Groups Claim

This approach adds the groups claim through your authorization server, providing more flexibility for complex configurations.
Comment thread
greptile-apps[bot] marked this conversation as resolved.

1. Navigate to **Security** → **API** → **Authorization Servers**
Expand All @@ -235,25 +183,9 @@ Configure the groups claim:

5. Click **Create**

You can also configure an additional groups claim in the application's Sign On settings:

1. Navigate to your application's **Sign On** tab

<Frame>
<img src="/media/user-provisioning/okta-group-configuration.png" alt="Application Sign On configuration" />
</Frame>

2. Under **OpenID Connect ID Token**, configure:
- **Groups claim type**: Expression
- **Groups claim expression**: `Arrays.flatten(Groups.startsWith("OKTA", "bifrost", 100))`

<Note>
Adjust the group filter expression based on your naming convention. The example above includes groups starting with "bifrost".
</Note>

---

## Step 7: Assign Users to the Application
## Step 6: Assign Users to the Application

1. Navigate to your application's **Assignments** tab

Expand All @@ -263,7 +195,9 @@ Adjust the group filter expression based on your naming convention. The example

2. Click **Assign** → **Assign to People** or **Assign to Groups**

3. For each user, set their **bifrostRole**:
### For Assigning Roles

For each user, set their **bifrostRole** (if you are planning to do role-level mapping):

<Frame>
<img src="/media/user-provisioning/okta-assign-custom-role.png" alt="Assign custom role to user" />
Expand All @@ -277,6 +211,22 @@ Role claims are available only when you configure custom claims on your authoriz

---

## Step 7: Create API token for bulk user and team sync

To create an API token, navigate to **Security** → **API** → **Tokens**.

<Frame>
<img src="/media/user-provisioning/okta-tokens-screen.png" alt="Okta API tokens screen" />
</Frame>

1. Click on "Create token"

<Frame>
<img src="/media/user-provisioning/okta-create-token-form.png" alt="Create token dialog in Okta" />
</Frame>

2. Copy token to be used in the next step.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

## Step 8: Configure Bifrost

Now configure Bifrost to use Okta as the identity provider.
Expand All @@ -297,9 +247,9 @@ Now configure Bifrost to use Okta as the identity provider.
4. Toggle **Enabled** to activate the provider
5. Click **Save Configuration**

### Group-to-Role Mappings (Optional)
### Group-to-Role Mappings

If you configured groups in Okta (Step 6), you can map Okta group names directly to Bifrost roles. This is an alternative to using custom role claims (Steps 4-5) and works with all Okta plans.
If you configured groups in Okta (Step 5), you can map Okta group names directly to Bifrost roles. This is an alternative to using custom role claims (Steps 3-4) and works with all Okta plans.

1. In the User Provisioning configuration, scroll down to **Group-to-Role Mappings**
2. Click **Add Mapping**
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 7 additions & 10 deletions framework/configstore/rdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func getWeight(w *float64) float64 {
return *w
}

// schemaKeyFromTableKey converts a database key to a schema key.
func schemaKeyFromTableKey(dbKey tables.TableKey) schemas.Key {
return schemas.Key{
ID: dbKey.KeyID,
Expand All @@ -59,6 +60,7 @@ func schemaKeyFromTableKey(dbKey tables.TableKey) schemas.Key {
}
}

// tableKeyFromSchemaKey converts a schema key to a database key.
func tableKeyFromSchemaKey(provider tables.TableProvider, key schemas.Key) (tables.TableKey, error) {
dbKey := tables.TableKey{
Provider: provider.Name,
Expand Down Expand Up @@ -175,13 +177,10 @@ func (s *RDBConfigStore) parseGormError(err error) error {
if err == nil {
return nil
}

if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrNotFound
}

errMsg := err.Error()

// Check for unique constraint violations
// SQLite format: "UNIQUE constraint failed: table_name.column_name"
// PostgreSQL format: "ERROR: duplicate key value violates unique constraint"
Expand Down Expand Up @@ -914,24 +913,20 @@ func (s *RDBConfigStore) CreateProviderKey(ctx context.Context, provider schemas
} else {
txDB = s.db
}

var dbProvider tables.TableProvider
if err := txDB.WithContext(ctx).Where("name = ?", string(provider)).First(&dbProvider).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrNotFound
}
return err
}

dbKey, err := tableKeyFromSchemaKey(dbProvider, key)
if err != nil {
return err
}

if err := txDB.WithContext(ctx).Create(&dbKey).Error; err != nil {
return s.parseGormError(err)
}

return nil
}

Expand Down Expand Up @@ -1859,10 +1854,11 @@ func preloadCustomerRelations(db *gorm.DB, prefix string) *gorm.DB {

// preloadVirtualKeyBaseRelations preloads the base relationships for a virtual key.
func preloadVirtualKeyBaseRelations(db *gorm.DB) *gorm.DB {
db = db.Preload("Team").Preload("Team.Customer")
db = db.Preload("Customer")
return db.
Preload("Budget").
Preload("Team").
Preload("Team.Customer").
Preload("Customer").
Preload("Budgets").
Preload("RateLimit").
Preload("ProviderConfigs").
Preload("ProviderConfigs.Budgets").
Expand Down Expand Up @@ -3163,6 +3159,7 @@ func (s *RDBConfigStore) GetModelConfigs(ctx context.Context) ([]tables.TableMod
return modelConfigs, nil
}

// GetModelConfigsPaginated retrieves model configs with pagination, filtering, and search support.
func (s *RDBConfigStore) GetModelConfigsPaginated(ctx context.Context, params ModelConfigsQueryParams) ([]tables.TableModelConfig, int64, error) {
baseQuery := s.db.WithContext(ctx).Model(&tables.TableModelConfig{})

Expand Down
10 changes: 0 additions & 10 deletions plugins/governance/test_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import (
configstoreTables "github.com/maximhq/bifrost/framework/configstore/tables"
"github.com/maximhq/bifrost/framework/modelcatalog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// MockLogger implements schemas.Logger for testing
Expand Down Expand Up @@ -244,15 +243,6 @@ func assertRateLimitInfo(t *testing.T, result *EvaluationResult) {
assert.NotNil(t, result.RateLimitInfo, "RateLimitInfo should be present in result")
}

func requireNoError(t *testing.T, err error, msg string) {
t.Helper()
require.NoError(t, err, msg)
}

func requireError(t *testing.T, err error, msg string) {
t.Helper()
require.Error(t, err, msg)
}

func buildModelConfig(id, modelName string, provider *string, budget *configstoreTables.TableBudget, rateLimit *configstoreTables.TableRateLimit) *configstoreTables.TableModelConfig {
mc := &configstoreTables.TableModelConfig{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Users } from "lucide-react";
import ContactUsView from "../views/contactUsView";

export function BusinessUnitsView() {
return (
<div className="w-full">
<ContactUsView
className="mx-auto min-h-[80vh]"
icon={<Users className="h-[5.5rem] w-[5.5rem]" strokeWidth={1} />}
title="Unlock advanced governance"
description="Manage users, business units with our enterprise-grade governance. This feature is part of the Bifrost enterprise license."
readmeLink="https://docs.getbifrost.ai/enterprise/advanced-governance"
/>
</div>
);
}
16 changes: 16 additions & 0 deletions ui/app/_fallbacks/enterprise/components/user-groups/teamsView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Users } from "lucide-react";
import ContactUsView from "../views/contactUsView";

export function TeamsView() {
return (
<div className="w-full">
<ContactUsView
className="mx-auto min-h-[80vh]"
icon={<Users className="h-[5.5rem] w-[5.5rem]" strokeWidth={1} />}
title="Unlock teams governance"
description="Manage teams, sync from your identity provider, and control access with enterprise-grade governance. This feature is part of the Bifrost enterprise license."
readmeLink="https://docs.getbifrost.ai/enterprise/advanced-governance"
/>
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,16 @@ interface ModelFilterSelectProps {
"data-testid"?: string;
}

export function ModelFilterSelect({ models, selectedModel, onModelChange, placeholder = "All Models", "data-testid": testId }: ModelFilterSelectProps) {
export function ModelFilterSelect({
models,
selectedModel,
onModelChange,
placeholder = "All Models",
"data-testid": testId,
}: ModelFilterSelectProps) {
return (
<Select value={selectedModel} onValueChange={onModelChange}>
<SelectTrigger className="w-[110px] text-xs sm:w-[130px] !h-7.5" data-testid={testId} size="sm">
<SelectTrigger className="!h-7.5 w-[110px] text-xs sm:w-[130px]" data-testid={testId} size="sm">
<SelectValue placeholder={placeholder} />
</SelectTrigger>
<SelectContent>
Expand Down
11 changes: 11 additions & 0 deletions ui/app/workspace/governance/business-units/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"use client";

import { BusinessUnitsView } from "@enterprise/components/user-groups/businessUnitsView";

export default function GovernanceBusinessUnitsPage() {
return (
<div className="mx-auto w-full max-w-7xl">
<BusinessUnitsView />
</div>
);
}
Loading
Loading