diff --git a/README.md b/README.md index 8391dbdb382..67e27bd1bd7 100644 --- a/README.md +++ b/README.md @@ -22,21 +22,19 @@ You can see live demo of CloudBeaver here: https://demo.cloudbeaver.io ## Changelog + +### 23.3.2. 2024-01-22 +- Added password policy for the local authorization. Password parameters can be set in the configuration file; +- The 'Keep alive' option has been added to the connection settings to keep the connection active even in case of inactivity; +- Added ability to display full text for a string data type in value panel; +- The DuckDB driver has been added; +- Different bug fixes and enhancements have been made. + ### 23.3.2. 2024-01-08 - Added the ability to view decoded binary-type data in the Value panel; - Enhanced security for unauthorized access; - Different bug fixes and enhancements have been made. -### 23.3.1. 2023-12-25 -- Performance: - - Upgraded to Jetty 11, delivering improved performance, enhanced features, and better alignment with the latest Java specifications. -- Resource management: - - Read-only scripts now have a padlock icon. -- UX improvement: - - Added validation for mandatory fields in all forms for creating and editing entities. -- Driver management: - - Apache Derby driver has been removed because of the vulnerability issues. -- Many small bug fixes, enhancements, and improvements have been made ### Old CloudBeaver releases diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/provisioning/SMProvisioner.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/provisioning/SMProvisioner.java index 7c40493752f..291b73ea1a0 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/provisioning/SMProvisioner.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/provisioning/SMProvisioner.java @@ -31,4 +31,8 @@ List listExternalUsers( @NotNull SMAuthProviderCustomConfiguration customConfiguration, @NotNull SMProvisioningFilter filter ) throws DBException; + + default boolean isAuthRoleProvided(SMAuthProviderCustomConfiguration configuration) { + return false; + } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebAuthApplication.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebAuthApplication.java index fae36cfecb9..4817849c8da 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebAuthApplication.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebAuthApplication.java @@ -20,6 +20,8 @@ import io.cloudbeaver.auth.CBAuthConstants; import org.jkiss.dbeaver.DBException; +import java.util.List; + public interface WebAuthApplication extends WebApplication { WebAuthConfiguration getAuthConfiguration(); @@ -30,4 +32,6 @@ default long getMaxSessionIdleTime() { } void flushConfiguration() throws DBException; + + String getDefaultAuthRole(); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/user/WebAuthProviderInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/user/WebAuthProviderInfo.java index 2a6a036f57e..c3c211c9701 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/user/WebAuthProviderInfo.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/user/WebAuthProviderInfo.java @@ -86,6 +86,12 @@ public boolean isPrivate() { public boolean isRequired() { return descriptor.isRequired(); } + public boolean isAuthRoleProvided(SMAuthProviderCustomConfiguration configuration) { + if (descriptor.getInstance() instanceof SMProvisioner provisioner) { + return provisioner.isAuthRoleProvided(configuration); + } + return false; + } public boolean isSupportProvisioning() { return descriptor.getInstance() instanceof SMProvisioner; diff --git a/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls b/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls index 0f4fca656ff..06085838cf7 100644 --- a/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls +++ b/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls @@ -30,6 +30,7 @@ type AuthProviderConfiguration { id: ID! displayName: String! disabled: Boolean! + authRoleProvided: Boolean iconURL: String description: String diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java index c7ab1323455..b3fffe8bc09 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java @@ -133,10 +133,6 @@ public void createUser( try (Connection dbCon = database.openConnection()) { try (JDBCTransaction txn = new JDBCTransaction(dbCon)) { createUser(dbCon, userId, metaParameters, enabled, defaultAuthRole); - String defaultTeamName = application.getAppConfiguration().getDefaultUserTeam(); - if (!CommonUtils.isEmpty(defaultTeamName)) { - setUserTeams(dbCon, userId, new String[]{defaultTeamName}, userId); - } txn.commit(); } } catch (SQLException e) { @@ -167,31 +163,32 @@ public void createUser( dbStat.execute(); } saveSubjectMetas(dbCon, userId, metaParameters); - + String defaultTeamName = application.getAppConfiguration().getDefaultUserTeam(); + if (!CommonUtils.isEmpty(defaultTeamName)) { + setUserTeams(dbCon, userId, new String[]{defaultTeamName}, userId); + } } @Override public void importUsers(@NotNull SMUserImportList userImportList) throws DBException { - for (SMUserProvisioning user : userImportList.getUsers()) { - if (isSubjectExists(user.getUserId())) { - log.info("Skip already exist user: " + user.getUserId()); - setUserAuthRole(user.getUserId(), userImportList.getAuthRole()); - continue; - } - createUser(user.getUserId(), user.getMetaParameters(), true, userImportList.getAuthRole()); - } + try (var dbCon = database.openConnection()) { + importUsers(dbCon, userImportList); + } catch (SQLException e) { + log.error("Failed attempt import user: " + e.getMessage()); + } } protected void importUsers(@NotNull Connection connection, @NotNull SMUserImportList userImportList) throws DBException, SQLException { for (SMUserProvisioning user : userImportList.getUsers()) { + String authRole = user.getAuthRole() == null ? userImportList.getAuthRole() : user.getAuthRole(); if (isSubjectExists(user.getUserId())) { log.info("User already exist : " + user.getUserId()); - setUserAuthRole(connection, user.getUserId(), userImportList.getAuthRole()); + setUserAuthRole(connection, user.getUserId(), authRole); enableUser(connection, user.getUserId(), true); continue; } - createUser(connection, user.getUserId(), user.getMetaParameters(), true, userImportList.getAuthRole()); + createUser(connection, user.getUserId(), user.getMetaParameters(), true, authRole); } } diff --git a/webapp/packages/core-blocks/src/Icon.test.tsx b/webapp/packages/core-blocks/src/Icon.test.tsx index 67c83209fd7..4e4b30ef806 100644 --- a/webapp/packages/core-blocks/src/Icon.test.tsx +++ b/webapp/packages/core-blocks/src/Icon.test.tsx @@ -13,20 +13,20 @@ import { Icon } from './Icon'; test('icons.svg#name', () => { (globalThis as any)._ROOT_URI_ = undefined; - render(); + render(); expect(screen.getByTestId('Icon').querySelector('use')).toHaveAttribute('href', '/icons/icons.svg#test'); }); test('/image.jpg', () => { (globalThis as any)._ROOT_URI_ = undefined; - render(); + render(); expect(screen.getByTestId('Icon').querySelector('use')).toHaveAttribute('href', '/image.jpg'); }); test('{_ROOT_URI_}/icons.svg#name', () => { (globalThis as any)._ROOT_URI_ = '/path/'; - render(); + render(); expect(screen.getByTestId('Icon').querySelector('use')).toHaveAttribute('href', '/path/icons/icons.svg#test'); }); diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeIcon.m.css b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeIcon.m.css index 20eb274d119..8dd258248a7 100644 --- a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeIcon.m.css +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeIcon.m.css @@ -1,6 +1,7 @@ .treeNodeIcon { position: relative; box-sizing: border-box; + pointer-events: none; flex-shrink: 0; width: 16px; height: 16px; diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeNested.tsx b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeNested.tsx index b379a6922ab..1abdf777173 100644 --- a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeNested.tsx +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeNested.tsx @@ -13,7 +13,6 @@ import { useS } from '../../useS'; import style from './TreeNodeNested.m.css'; interface Props extends React.PropsWithChildren { - expanded?: boolean; root?: boolean; className?: string; } diff --git a/webapp/packages/core-cli/configs/jest.babel.config.js b/webapp/packages/core-cli/configs/jest.babel.config.js index 8d2b074b305..8eaf66a9932 100644 --- a/webapp/packages/core-cli/configs/jest.babel.config.js +++ b/webapp/packages/core-cli/configs/jest.babel.config.js @@ -7,5 +7,5 @@ */ module.exports = { presets: ['@babel/preset-env', ['@babel/preset-react', { runtime: 'automatic' }]], - plugins: ['../dist/babel-plugins/TestingAttributes.js', require('@reshadow/babel')], + plugins: [require('@reshadow/babel')], }; diff --git a/webapp/packages/core-cli/tests/test.environment.js b/webapp/packages/core-cli/tests/test.environment.js index 407fcdb17b2..bd7ec944f4a 100644 --- a/webapp/packages/core-cli/tests/test.environment.js +++ b/webapp/packages/core-cli/tests/test.environment.js @@ -12,5 +12,8 @@ module.exports = class CustomTestEnvironment extends Environment { this.global.TextDecoder = TextDecoder; this.global.Response = Response; this.global.Request = Request; + + // different machine has its own timezones and some tests can fail because of it + process.env.TZ = 'UTC'; } -}; \ No newline at end of file +}; diff --git a/webapp/packages/core-sdk/src/queries/fragments/AuthProviderConfigurationInfo.gql b/webapp/packages/core-sdk/src/queries/fragments/AuthProviderConfigurationInfo.gql index 64ef403ca4a..99c0e023f4a 100644 --- a/webapp/packages/core-sdk/src/queries/fragments/AuthProviderConfigurationInfo.gql +++ b/webapp/packages/core-sdk/src/queries/fragments/AuthProviderConfigurationInfo.gql @@ -1,6 +1,7 @@ fragment AuthProviderConfigurationInfo on AuthProviderConfiguration { id displayName + authRoleProvided iconURL description signInLink diff --git a/webapp/packages/core-ui/src/DragAndDrop/useDNDData.ts b/webapp/packages/core-ui/src/DragAndDrop/useDNDData.ts index ab46528e1ca..7c3cfe6ec1e 100644 --- a/webapp/packages/core-ui/src/DragAndDrop/useDNDData.ts +++ b/webapp/packages/core-ui/src/DragAndDrop/useDNDData.ts @@ -68,7 +68,7 @@ export function useDNDData(context: IDataContextProvider, options: IOptions = {} } } - state.isDragging = monitor.isDragging(); + state.isDragging = dragging; }, })); diff --git a/webapp/packages/core-utils/src/TempMap.ts b/webapp/packages/core-utils/src/TempMap.ts index cf63ec3d775..144d3c29ff4 100644 --- a/webapp/packages/core-utils/src/TempMap.ts +++ b/webapp/packages/core-utils/src/TempMap.ts @@ -23,7 +23,7 @@ export class TempMap implements Map { return 'TempMap'; } - private readonly deleted: TKey[]; + private readonly deleted: Map; private readonly temp: Map; private flushTask: NodeJS.Timeout | null; private readonly keysTemp: ICachedValueObject; @@ -33,7 +33,7 @@ export class TempMap implements Map { constructor(private readonly target: Map, private readonly onSync?: () => void) { this.temp = new Map(); this.flushTask = null; - this.deleted = []; + this.deleted = new Map(); this.keysTemp = cacheValue(); this.entriesTemp = cacheValue(); this.valuesTemp = cacheValue(); @@ -45,7 +45,7 @@ export class TempMap implements Map { } isDeleted(key: TKey): boolean { - return this.deleted.includes(key); + return this.deleted.get(key) || false; } /** @@ -56,7 +56,7 @@ export class TempMap implements Map { clearTimeout(this.flushTask); this.flushTask = null; } - this.deleted.splice(0, this.deleted.length); + this.deleted.clear(); this.temp.clear(); this.keysTemp.invalidate(); this.valuesTemp.invalidate(); @@ -65,7 +65,7 @@ export class TempMap implements Map { delete(key: TKey): boolean { this.temp.delete(key); - this.deleted.push(key); + this.deleted.set(key, true); this.scheduleFlush(); return this.has(key); } @@ -105,10 +105,7 @@ export class TempMap implements Map { set(key: TKey, value: TValue): this { this.temp.set(key, value); - const indexOfDeleted = this.deleted.indexOf(key); - if (indexOfDeleted !== -1) { - this.deleted.splice(indexOfDeleted, 1); - } + this.deleted.delete(key); this.scheduleFlush(); return this; @@ -139,10 +136,10 @@ export class TempMap implements Map { this.flushTask = setTimeout( action(() => { - for (const deleted of this.deleted) { + for (const [deleted] of this.deleted) { this.target.delete(deleted); } - this.deleted.splice(0, this.deleted.length); + this.deleted.clear(); for (const [key, value] of this.temp) { this.target.set(key, value); diff --git a/webapp/packages/core-utils/src/cacheValue.test.ts b/webapp/packages/core-utils/src/cacheValue.test.ts new file mode 100644 index 00000000000..45dacb88901 --- /dev/null +++ b/webapp/packages/core-utils/src/cacheValue.test.ts @@ -0,0 +1,47 @@ +import { cacheValue } from './cacheValue'; + +describe('cacheValue', () => { + it('should return cached value', () => { + const cache = cacheValue(); + const value = cache.value(() => 1); + expect(value).toBe(1); + expect(cache.invalid).toBe(false); + }); + + it('should invalidate cache', () => { + const cache = cacheValue(); + cache.value(() => 1); + cache.invalidate(); + expect(cache.invalid).toBe(true); + }); + + it('should calculate new value if invalidated', () => { + const fn = jest.fn(() => 1); + const cache = cacheValue(); + cache.value(fn); + cache.invalidate(); + expect(cache.invalid).toBe(true); + const value = cache.value(fn); + expect(value).toBe(1); + expect(cache.invalid).toBe(false); + expect(fn).toHaveBeenCalledTimes(2); + }); + + it('should not calculate new value if not invalidated', () => { + const fn = jest.fn(() => 1); + const cache = cacheValue(); + cache.value(fn); + const value = cache.value(fn); + expect(value).toBe(1); + expect(cache.invalid).toBe(false); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('should cache value until it is invalidated', () => { + const cache = cacheValue(); + expect(cache.value(() => 1)).toBe(1); + expect(cache.value(() => 2)).toBe(1); + cache.invalidate(); + expect(cache.value(() => 3)).toBe(3); + }); +}); diff --git a/webapp/packages/core-utils/src/timestampToDate.test.ts b/webapp/packages/core-utils/src/timestampToDate.test.ts new file mode 100644 index 00000000000..80b307c5daf --- /dev/null +++ b/webapp/packages/core-utils/src/timestampToDate.test.ts @@ -0,0 +1,18 @@ +import { timestampToDate } from './timestampToDate'; + +describe('timestampToDate', () => { + it('should convert timestamp to date', () => { + const date = timestampToDate(1591862400000); + expect(date).toBe('6/11/2020, 8:00:00 AM'); + }); + + it('should convert negative timestamp to date', () => { + const date = timestampToDate(-1591862400000); + expect(date).toBe('7/23/1919, 4:00:00 PM'); + }); + + it('should convert zero timestamp to date', () => { + const date = timestampToDate(0); + expect(date).toBe('1/1/1970, 12:00:00 AM'); + }); +}); diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/useNavigationNode.ts b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/useNavigationNode.ts index 7f296afb283..d1d72f5ad36 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/useNavigationNode.ts +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/useNavigationNode.ts @@ -9,7 +9,6 @@ import React, { useContext, useEffect, useRef } from 'react'; import { getComputed, useExecutor, useObjectRef } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; -import { SyncExecutor } from '@cloudbeaver/core-executor'; import { EObjectFeature, type NavNode, NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree'; import { resourceKeyList } from '@cloudbeaver/core-resource'; import type { IDNDData } from '@cloudbeaver/core-ui'; @@ -17,7 +16,6 @@ import type { IDNDData } from '@cloudbeaver/core-ui'; import { useChildren } from '../../../NodesManager/useChildren'; import { useNode } from '../../../NodesManager/useNode'; import { ElementsTreeContext } from '../ElementsTreeContext'; -import type { IElementsTreeAction } from '../IElementsTreeAction'; import type { NavTreeControlComponent } from '../NavigationNodeComponent'; import type { IElementsTree } from '../useElementsTree'; @@ -94,7 +92,7 @@ export function useNavigationNode(node: NavNode, path: string[]): INavigationNod }, []); useExecutor({ - executor: contextRef.context?.tree.actions || new SyncExecutor(), + executor: contextRef.context?.tree.actions, handlers: [ function refreshRoot({ type, nodeId }) { if (type === 'show' && nodeId === node.id) {