Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ ladle-build
result/
.aider*
.env
.zellij_layout.kdl
111 changes: 111 additions & 0 deletions crates/defguard_core/src/enterprise/handlers/openid_providers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -328,11 +328,83 @@ pub(crate) async fn modify_openid_provider(
let mut transaction = appstate.pool.begin().await?;
let provider = OpenIdProvider::find_by_name(&mut *transaction, &provider_data.name).await?;
if let Some(mut provider) = provider {
let private_key = match &provider_data.google_service_account_key {
Some(key) => {
if RsaPrivateKey::from_pkcs8_pem(key).is_ok() {
debug!(
"User {} provided a valid RSA private key for provider's directory sync. Using it.",
session.user.username
);
provider_data.google_service_account_key.clone()
} else {
debug!(
"User {} did not provide a valid RSA private key for provider's directory sync or the key did not change. Using the existing key",
session.user.username
);
provider.google_service_account_key.clone()
}
}
None => provider.google_service_account_key.clone(),
};

let okta_private_jwk = match &provider_data.okta_private_jwk {
Some(key) => {
if serde_json::from_str::<serde_json::Value>(key).is_ok() {
debug!(
"User {} provided a valid JWK private key for provider's Okta directory sync. Using it.",
session.user.username
);
provider_data.okta_private_jwk.clone()
} else {
debug!(
"User {} did not provide a valid JWK private key for provider's Okta directory sync or the key did not change. Using the existing key.",
session.user.username
);
provider.okta_private_jwk.clone()
}
}
None => provider.okta_private_jwk.clone(),
};

let mut settings = Settings::get_current_settings();
settings.openid_create_account = provider_data.create_account;
settings.openid_username_handling = provider_data.username_handling;
update_current_settings(&appstate.pool, settings).await?;

let group_match = if let Some(group_match) = provider_data.directory_sync_group_match {
if group_match.is_empty() {
Vec::new()
} else {
group_match
.split(',')
.map(|s| s.trim().to_string())
.collect()
}
} else {
Vec::new()
};

provider.base_url = provider_data.base_url;
provider.kind = provider_data.kind;
provider.client_id = provider_data.client_id;
provider.client_secret = provider_data.client_secret;
provider.display_name = provider_data.display_name;
provider.google_service_account_key = private_key;
provider.google_service_account_email = provider_data.google_service_account_email;
provider.admin_email = provider_data.admin_email;
provider.directory_sync_enabled = provider_data.directory_sync_enabled;
provider.directory_sync_interval = provider_data.directory_sync_interval;
provider.directory_sync_user_behavior = provider_data.directory_sync_user_behavior.into();
provider.directory_sync_admin_behavior = provider_data.directory_sync_admin_behavior.into();
provider.directory_sync_target = provider_data.directory_sync_target.into();
provider.okta_private_jwk = okta_private_jwk;
provider.okta_dirsync_client_id = provider_data.okta_dirsync_client_id;
provider.directory_sync_group_match = group_match;
provider.jumpcloud_api_key = provider_data.jumpcloud_api_key;
provider.prefetch_users = provider_data.prefetch_users;
provider.save(&mut *transaction).await?;
transaction.commit().await?;

info!(
"User {} modified OpenID client {}",
session.user.username, provider.name
Expand Down Expand Up @@ -372,6 +444,45 @@ pub(crate) async fn list_openid_providers(
Ok(ApiResponse::json(providers, StatusCode::OK))
}

/// Get current OpenID provider.
///
/// # Returns
/// - HTTP Status "OK" on success.
#[utoipa::path(
get,
path = "/api/v1/openid/provider/current",
tag = "OpenID",
responses(
(status = OK, description = "Get current OpenID provider"),
),
params(
("name" = String, Path, description = "The name of a provider",)
)
)]
pub(crate) async fn get_current_openid_provider(
_admin: AdminRole,
State(appstate): State<AppState>,
) -> ApiResult {
let settings = Settings::get_current_settings();
let settings_json = json!({"create_account": settings.openid_create_account,
"username_handling": settings.openid_username_handling});
match OpenIdProvider::get_current(&appstate.pool).await? {
Some(mut provider) => {
// Get rid of it, it should stay on the backend only.
provider.google_service_account_key = None;
provider.okta_private_jwk = None;
Ok(ApiResponse::new(
json!({"provider": provider, "settings": settings_json}),
StatusCode::OK,
))
}
None => Ok(ApiResponse::new(
json!({"provider": null, "settings": settings_json}),
StatusCode::NO_CONTENT,
)),
}
}

pub(crate) async fn test_dirsync_connection(
_license: LicenseInfo,
_admin: AdminRole,
Expand Down
6 changes: 4 additions & 2 deletions crates/defguard_core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,9 @@ use crate::{
enterprise_settings::{get_enterprise_settings, patch_enterprise_settings},
openid_login::{auth_callback, get_auth_info},
openid_providers::{
add_openid_provider, delete_openid_provider, get_openid_provider,
list_openid_providers, modify_openid_provider, test_dirsync_connection,
add_openid_provider, delete_openid_provider, get_current_openid_provider,
get_openid_provider, list_openid_providers, modify_openid_provider,
test_dirsync_connection,
},
},
snat::handlers::{
Expand Down Expand Up @@ -394,6 +395,7 @@ pub fn build_webapp(
.put(modify_openid_provider)
.delete(delete_openid_provider),
)
.route("/provider/current", get(get_current_openid_provider))
.route("/callback", post(auth_callback))
.route("/auth_info", get(get_auth_info)),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { SizedBox } from '../../../../../shared/defguard-ui/components/SizedBox/
import { ThemeSpacing } from '../../../../../shared/defguard-ui/types';
import { useAppForm } from '../../../../../shared/form';
import { formChangeLogic } from '../../../../../shared/formLogic';
import { joinCsv } from '../../../../../shared/utils/csv';
import {
directorySyncBehaviorOptions,
directorySyncTargetOptions,
Expand All @@ -29,7 +30,7 @@ export const MicrosoftProviderForm = ({ onSubmit }: ProviderFormProps) => {
const defaultValues = useMemo(
(): FormFields => ({
directory_sync_admin_behavior: providerState.directory_sync_admin_behavior,
directory_sync_group_match: providerState.directory_sync_group_match ?? null,
directory_sync_group_match: joinCsv(providerState.directory_sync_group_match),
directory_sync_interval: providerState.directory_sync_interval,
directory_sync_target: providerState.directory_sync_target,
directory_sync_user_behavior: providerState.directory_sync_user_behavior,
Expand All @@ -46,7 +47,10 @@ export const MicrosoftProviderForm = ({ onSubmit }: ProviderFormProps) => {
onChange: microsoftProviderSyncSchema,
},
onSubmit: async ({ value }) => {
await onSubmit(value);
await onSubmit({
...value,
directory_sync_group_match: value.directory_sync_group_match ?? '',
});
},
});

Expand Down Expand Up @@ -113,10 +117,18 @@ export const MicrosoftProviderForm = ({ onSubmit }: ProviderFormProps) => {
<ProviderFormControls
loading={isPending}
onBack={() => {
back(form.state.values);
back({
...form.state.values,
directory_sync_group_match:
form.state.values.directory_sync_group_match ?? '',
});
}}
onNext={() => {
mutate(form.state.values);
mutate({
...form.state.values,
directory_sync_group_match:
form.state.values.directory_sync_group_match ?? '',
});
}}
/>
</form.AppForm>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const googleProviderSyncSchema = baseExternalProviderSyncSchema.extend({

export const microsoftProviderSyncSchema = baseExternalProviderSyncSchema.extend({
prefetch_users: z.boolean(),
directory_sync_group_match: z.string().trim().nullable(),
directory_sync_group_match: z.string().trim(),
});

export const oktaProviderSyncSchema = baseExternalProviderSyncSchema.extend({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export const useAddExternalOpenIdStore = create<Store>()(
});
},
initialize: (provider) => {
const initialProviderState = addExternalOpenIdStoreDefaults.providerState;
const initialProviderState = { ...addExternalOpenIdStoreDefaults.providerState };
initialProviderState.name = provider;
initialProviderState.kind = provider;
if (provider !== OpenIdProviderKind.Custom) {
Expand All @@ -137,6 +137,7 @@ export const useAddExternalOpenIdStore = create<Store>()(
break;
}
set({
provider,
activeStep: 'client-settings',
providerState: initialProviderState,
});
Expand Down
10 changes: 8 additions & 2 deletions web/src/pages/AliasesPage/AliasesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Suspense, useMemo, useState } from 'react';
import { AclDeploymentState, type AclDeploymentStateValue } from '../../shared/api/types';
import { Page } from '../../shared/components/Page/Page';
import { TableSkeleton } from '../../shared/components/skeleton/TableSkeleton/TableSkeleton';
import { IconKind } from '../../shared/defguard-ui/components/Icon';
import { Tabs } from '../../shared/defguard-ui/components/Tabs/Tabs';
import type { TabsItem } from '../../shared/defguard-ui/components/Tabs/types';
import { TablePageLayout } from '../../shared/layout/TablePageLayout/TablePageLayout';
Expand All @@ -18,6 +19,10 @@ export const AliasesPage = () => {
AclDeploymentState.Applied,
);

const pendingCount = aliasesCount?.pending ?? 0;
const pendingTitle = pendingCount ? `Pending (${pendingCount})` : 'Pending';
const pendingIcon = pendingCount > 0 ? IconKind.AttentionFilled : undefined;

const tabs = useMemo(
(): TabsItem[] => [
{
Expand All @@ -32,10 +37,11 @@ export const AliasesPage = () => {
onClick: () => {
setActiveTab(AclDeploymentState.Modified);
},
title: aliasesCount?.pending ? `Pending (${aliasesCount.pending})` : 'Pending',
title: pendingTitle,
icon: pendingIcon,
},
],
[activeTab, aliasesCount],
[activeTab, pendingIcon, pendingTitle],
);

return (
Expand Down
2 changes: 2 additions & 0 deletions web/src/pages/CEAliasPage/CEAliasPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { Button } from '../../shared/defguard-ui/components/Button/Button';
import { Divider } from '../../shared/defguard-ui/components/Divider/Divider';
import { MarkedSection } from '../../shared/defguard-ui/components/MarkedSection/MarkedSection';
import { SizedBox } from '../../shared/defguard-ui/components/SizedBox/SizedBox';
import { Snackbar } from '../../shared/defguard-ui/providers/snackbar/snackbar';
import { TooltipContent } from '../../shared/defguard-ui/providers/tooltip/TooltipContent';
import { TooltipProvider } from '../../shared/defguard-ui/providers/tooltip/TooltipContext';
import { TooltipTrigger } from '../../shared/defguard-ui/providers/tooltip/TooltipTrigger';
Expand Down Expand Up @@ -141,6 +142,7 @@ const FormContent = ({ alias }: { alias?: AclAlias }) => {
} else {
await addAlias(toSend);
}
Snackbar.default('Aliases added to Pending tab and awaiting deployment.');
router.history.back();
},
});
Expand Down
4 changes: 2 additions & 2 deletions web/src/pages/CEDestinationPage/CEDestinationPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export const CEDestinationPage = ({ destination }: Props) => {
const { mutateAsync: addDestination } = useMutation({
mutationFn: api.acl.destination.addDestination,
onSuccess: () => {
Snackbar.success('Destination added');
Snackbar.default('Destinations added to Pending tab and awaiting deployment.');
},
onError: (e) => {
Snackbar.error('Error occurred');
Expand All @@ -94,7 +94,7 @@ export const CEDestinationPage = ({ destination }: Props) => {
const { mutateAsync: editDestination } = useMutation({
mutationFn: api.acl.destination.editDestination,
onSuccess: () => {
Snackbar.success('Destination modified');
Snackbar.default('Destinations added to Pending tab and awaiting deployment.');
},
onError: (e) => {
Snackbar.error('Error occurred');
Expand Down
4 changes: 2 additions & 2 deletions web/src/pages/CERulePage/CERulePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ const Content = ({ rule: initialRule }: Props) => {
invalidate: ['acl'],
},
onSuccess: () => {
Snackbar.success('Rule added');
Snackbar.default('Rules added to Pending tab and awaiting deployment.');
router.history.back();
},
});
Expand All @@ -162,7 +162,7 @@ const Content = ({ rule: initialRule }: Props) => {
invalidate: ['acl'],
},
onSuccess: () => {
Snackbar.success('Rule changed');
Snackbar.default('Rules added to Pending tab and awaiting deployment.');
router.history.back();
},
});
Expand Down
12 changes: 8 additions & 4 deletions web/src/pages/DestinationsPage/DestinationsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Suspense, useMemo, useState } from 'react';
import { AclDeploymentState, type AclDeploymentStateValue } from '../../shared/api/types';
import { Page } from '../../shared/components/Page/Page';
import { TableSkeleton } from '../../shared/components/skeleton/TableSkeleton/TableSkeleton';
import { IconKind } from '../../shared/defguard-ui/components/Icon';
import { Tabs } from '../../shared/defguard-ui/components/Tabs/Tabs';
import type { TabsItem } from '../../shared/defguard-ui/components/Tabs/types';
import { TablePageLayout } from '../../shared/layout/TablePageLayout/TablePageLayout';
Expand All @@ -17,6 +18,10 @@ export const DestinationsPage = () => {
AclDeploymentState.Applied,
);

const pendingCount = destinationsCount?.pending ?? 0;
const pendingTitle = pendingCount ? `Pending (${pendingCount})` : 'Pending';
const pendingIcon = pendingCount > 0 ? IconKind.AttentionFilled : undefined;

const tabs = useMemo(
(): TabsItem[] => [
{
Expand All @@ -31,12 +36,11 @@ export const DestinationsPage = () => {
onClick: () => {
setActiveTab(AclDeploymentState.Modified);
},
title: destinationsCount?.pending
? `Pending (${destinationsCount.pending})`
: 'Pending',
title: pendingTitle,
icon: pendingIcon,
},
],
[activeTab, destinationsCount],
[activeTab, pendingIcon, pendingTitle],
);

return (
Expand Down
10 changes: 7 additions & 3 deletions web/src/pages/RulesPage/RulesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useQuery } from '@tanstack/react-query';
import { Suspense, useMemo, useState } from 'react';
import { Page } from '../../shared/components/Page/Page';
import { TableSkeleton } from '../../shared/components/skeleton/TableSkeleton/TableSkeleton';
import { IconKind } from '../../shared/defguard-ui/components/Icon';
import { SizedBox } from '../../shared/defguard-ui/components/SizedBox/SizedBox';
import { Tabs } from '../../shared/defguard-ui/components/Tabs/Tabs';
import type { TabsItem } from '../../shared/defguard-ui/components/Tabs/types';
Expand All @@ -17,10 +18,12 @@ export const RulesPage = () => {

const { data: rulesCount } = useQuery(getRulesCountQueryOptions);

const pendingCount = rulesCount?.pending ?? 0;
const pendingTabTitle = useMemo(
() => `Pending${rulesCount?.pending ? ` (${rulesCount.pending})` : ''}`,
[rulesCount],
() => `Pending${pendingCount ? ` (${pendingCount})` : ''}`,
[pendingCount],
);
const pendingIcon = pendingCount > 0 ? IconKind.AttentionFilled : undefined;

const tabs = useMemo(
(): TabsItem[] => [
Expand All @@ -33,13 +36,14 @@ export const RulesPage = () => {
},
{
title: pendingTabTitle,
icon: pendingIcon,
active: activeTab === RulesPageTab.Pending,
onClick: () => {
setActiveTab(RulesPageTab.Pending);
},
},
],
[activeTab, pendingTabTitle],
[activeTab, pendingIcon, pendingTabTitle],
);

return (
Expand Down
Loading
Loading