Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(config): implementation of lexicon #49399

Merged
merged 2 commits into from
Dec 16, 2024
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
3 changes: 3 additions & 0 deletions lib/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
'NCU\\Config\\Exceptions\\TypeConflictException' => $baseDir . '/lib/unstable/Config/Exceptions/TypeConflictException.php',
'NCU\\Config\\Exceptions\\UnknownKeyException' => $baseDir . '/lib/unstable/Config/Exceptions/UnknownKeyException.php',
'NCU\\Config\\IUserConfig' => $baseDir . '/lib/unstable/Config/IUserConfig.php',
'NCU\\Config\\Lexicon\\ConfigLexiconEntry' => $baseDir . '/lib/unstable/Config/Lexicon/ConfigLexiconEntry.php',
'NCU\\Config\\Lexicon\\ConfigLexiconStrictness' => $baseDir . '/lib/unstable/Config/Lexicon/ConfigLexiconStrictness.php',
'NCU\\Config\\Lexicon\\IConfigLexicon' => $baseDir . '/lib/unstable/Config/Lexicon/IConfigLexicon.php',
'NCU\\Config\\ValueType' => $baseDir . '/lib/unstable/Config/ValueType.php',
'NCU\\Federation\\ISignedCloudFederationProvider' => $baseDir . '/lib/unstable/Federation/ISignedCloudFederationProvider.php',
'NCU\\Security\\Signature\\Enum\\DigestAlgorithm' => $baseDir . '/lib/unstable/Security/Signature/Enum/DigestAlgorithm.php',
Expand Down
3 changes: 3 additions & 0 deletions lib/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'NCU\\Config\\Exceptions\\TypeConflictException' => __DIR__ . '/../../..' . '/lib/unstable/Config/Exceptions/TypeConflictException.php',
'NCU\\Config\\Exceptions\\UnknownKeyException' => __DIR__ . '/../../..' . '/lib/unstable/Config/Exceptions/UnknownKeyException.php',
'NCU\\Config\\IUserConfig' => __DIR__ . '/../../..' . '/lib/unstable/Config/IUserConfig.php',
'NCU\\Config\\Lexicon\\ConfigLexiconEntry' => __DIR__ . '/../../..' . '/lib/unstable/Config/Lexicon/ConfigLexiconEntry.php',
'NCU\\Config\\Lexicon\\ConfigLexiconStrictness' => __DIR__ . '/../../..' . '/lib/unstable/Config/Lexicon/ConfigLexiconStrictness.php',
'NCU\\Config\\Lexicon\\IConfigLexicon' => __DIR__ . '/../../..' . '/lib/unstable/Config/Lexicon/IConfigLexicon.php',
'NCU\\Config\\ValueType' => __DIR__ . '/../../..' . '/lib/unstable/Config/ValueType.php',
'NCU\\Federation\\ISignedCloudFederationProvider' => __DIR__ . '/../../..' . '/lib/unstable/Federation/ISignedCloudFederationProvider.php',
'NCU\\Security\\Signature\\Enum\\DigestAlgorithm' => __DIR__ . '/../../..' . '/lib/unstable/Security/Signature/Enum/DigestAlgorithm.php',
Expand Down
122 changes: 122 additions & 0 deletions lib/private/AppConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@

use InvalidArgumentException;
use JsonException;
use NCU\Config\Lexicon\ConfigLexiconEntry;
use NCU\Config\Lexicon\ConfigLexiconStrictness;
use NCU\Config\Lexicon\IConfigLexicon;
use OC\AppFramework\Bootstrap\Coordinator;
use OCP\DB\Exception as DBException;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\Exceptions\AppConfigIncorrectTypeException;
Expand Down Expand Up @@ -55,6 +59,8 @@ class AppConfig implements IAppConfig {
private array $valueTypes = []; // type for all config values
private bool $fastLoaded = false;
private bool $lazyLoaded = false;
/** @var array<array-key, array{entries: array<array-key, ConfigLexiconEntry>, strictness: ConfigLexiconStrictness}> ['app_id' => ['strictness' => ConfigLexiconStrictness, 'entries' => ['config_key' => ConfigLexiconEntry[]]] */
private array $configLexiconDetails = [];

/**
* $migrationCompleted is only needed to manage the previous structure
Expand Down Expand Up @@ -430,6 +436,9 @@ private function getTypedValue(
int $type,
): string {
$this->assertParams($app, $key, valueType: $type);
if (!$this->matchAndApplyLexiconDefinition($app, $key, $lazy, $type, $default)) {
return $default; // returns default if strictness of lexicon is set to WARNING (block and report)
}
$this->loadConfig($app, $lazy);

/**
Expand Down Expand Up @@ -721,6 +730,9 @@ private function setTypedValue(
int $type,
): bool {
$this->assertParams($app, $key);
if (!$this->matchAndApplyLexiconDefinition($app, $key, $lazy, $type)) {
return false; // returns false as database is not updated
}
$this->loadConfig(null, $lazy);

$sensitive = $this->isTyped(self::VALUE_SENSITIVE, $type);
Expand Down Expand Up @@ -1559,4 +1571,114 @@ private function getSensitiveKeys(string $app): array {
public function clearCachedConfig(): void {
$this->clearCache();
}

/**
* match and apply current use of config values with defined lexicon
*
* @throws AppConfigUnknownKeyException
* @throws AppConfigTypeConflictException
* @return bool TRUE if everything is fine compared to lexicon or lexicon does not exist
*/
private function matchAndApplyLexiconDefinition(
string $app,
string $key,
bool &$lazy,
int &$type,
string &$default = '',
): bool {
if (in_array($key,
[
'enabled',
'installed_version',
'types',
])) {
return true; // we don't break stuff for this list of config keys.
}
$configDetails = $this->getConfigDetailsFromLexicon($app);
if (!array_key_exists($key, $configDetails['entries'])) {
return $this->applyLexiconStrictness(
$configDetails['strictness'],
'The app config key ' . $app . '/' . $key . ' is not defined in the config lexicon'
);
}

/** @var ConfigLexiconEntry $configValue */
$configValue = $configDetails['entries'][$key];
$type &= ~self::VALUE_SENSITIVE;

$appConfigValueType = $configValue->getValueType()->toAppConfigFlag();
if ($type === self::VALUE_MIXED) {
$type = $appConfigValueType; // we overwrite if value was requested as mixed
} elseif ($appConfigValueType !== $type) {
throw new AppConfigTypeConflictException('The app config key ' . $app . '/' . $key . ' is typed incorrectly in relation to the config lexicon');
}

$lazy = $configValue->isLazy();
$default = $configValue->getDefault() ?? $default; // default from Lexicon got priority
if ($configValue->isFlagged(self::FLAG_SENSITIVE)) {
$type |= self::VALUE_SENSITIVE;
}
if ($configValue->isDeprecated()) {
$this->logger->notice('App config key ' . $app . '/' . $key . ' is set as deprecated.');
}

return true;
}

/**
* manage ConfigLexicon behavior based on strictness set in IConfigLexicon
*
* @param ConfigLexiconStrictness|null $strictness
* @param string $line
*
* @return bool TRUE if conflict can be fully ignored, FALSE if action should be not performed
* @throws AppConfigUnknownKeyException if strictness implies exception
* @see IConfigLexicon::getStrictness()
*/
private function applyLexiconStrictness(
?ConfigLexiconStrictness $strictness,
string $line = '',
): bool {
if ($strictness === null) {
return true;
}

switch ($strictness) {
case ConfigLexiconStrictness::IGNORE:
return true;
case ConfigLexiconStrictness::NOTICE:
$this->logger->notice($line);
return true;
case ConfigLexiconStrictness::WARNING:
$this->logger->warning($line);
return false;
}

throw new AppConfigUnknownKeyException($line);
}

/**
* extract details from registered $appId's config lexicon
*
* @param string $appId
*
* @return array{entries: array<array-key, ConfigLexiconEntry>, strictness: ConfigLexiconStrictness}
*/
private function getConfigDetailsFromLexicon(string $appId): array {
if (!array_key_exists($appId, $this->configLexiconDetails)) {
$entries = [];
$bootstrapCoordinator = \OCP\Server::get(Coordinator::class);
$configLexicon = $bootstrapCoordinator->getRegistrationContext()?->getConfigLexicon($appId);
foreach ($configLexicon?->getAppConfigs() ?? [] as $configEntry) {
$entries[$configEntry->getKey()] = $configEntry;
}

$this->configLexiconDetails[$appId] = [
'entries' => $entries,
'strictness' => $configLexicon?->getStrictness() ?? ConfigLexiconStrictness::IGNORE
];
}

return $this->configLexiconDetails[$appId];
}
}
34 changes: 34 additions & 0 deletions lib/private/AppFramework/Bootstrap/RegistrationContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
namespace OC\AppFramework\Bootstrap;

use Closure;
use NCU\Config\Lexicon\IConfigLexicon;
use OC\Support\CrashReport\Registry;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IRegistrationContext;
Expand Down Expand Up @@ -141,6 +142,9 @@ class RegistrationContext {
/** @var ServiceRegistration<IDeclarativeSettingsForm>[] */
private array $declarativeSettings = [];

/** @var array<array-key, string> */
private array $configLexiconClasses = [];

/** @var ServiceRegistration<ITeamResourceProvider>[] */
private array $teamResourceProviders = [];

Expand Down Expand Up @@ -422,6 +426,13 @@ public function registerMailProvider(string $class): void {
$class
);
}

public function registerConfigLexicon(string $configLexiconClass): void {
$this->context->registerConfigLexicon(
$this->appId,
$configLexiconClass
);
}
};
}

Expand Down Expand Up @@ -621,6 +632,13 @@ public function registerMailProvider(string $appId, string $class): void {
$this->mailProviders[] = new ServiceRegistration($appId, $class);
}

/**
* @psalm-param class-string<IConfigLexicon> $configLexiconClass
*/
public function registerConfigLexicon(string $appId, string $configLexiconClass): void {
$this->configLexiconClasses[$appId] = $configLexiconClass;
}

/**
* @param App[] $apps
*/
Expand Down Expand Up @@ -972,4 +990,20 @@ public function getTaskProcessingTaskTypes(): array {
public function getMailProviders(): array {
return $this->mailProviders;
}

/**
* returns IConfigLexicon registered by the app.
* null if none registered.
*
* @param string $appId
*
* @return IConfigLexicon|null
*/
public function getConfigLexicon(string $appId): ?IConfigLexicon {
if (!array_key_exists($appId, $this->configLexiconClasses)) {
return null;
}

return \OCP\Server::get($this->configLexiconClasses[$appId]);
}
}
107 changes: 106 additions & 1 deletion lib/private/Config/UserConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
use NCU\Config\Exceptions\TypeConflictException;
use NCU\Config\Exceptions\UnknownKeyException;
use NCU\Config\IUserConfig;
use NCU\Config\Lexicon\ConfigLexiconEntry;
use NCU\Config\Lexicon\ConfigLexiconStrictness;
use NCU\Config\ValueType;
use OC\AppFramework\Bootstrap\Coordinator;
use OCP\DB\Exception as DBException;
use OCP\DB\IResult;
use OCP\DB\QueryBuilder\IQueryBuilder;
Expand Down Expand Up @@ -63,6 +66,8 @@ class UserConfig implements IUserConfig {
private array $fastLoaded = [];
/** @var array<string, boolean> ['user_id' => bool] */
private array $lazyLoaded = [];
/** @var array<array-key, array{entries: array<array-key, ConfigLexiconEntry>, strictness: ConfigLexiconStrictness}> ['app_id' => ['strictness' => ConfigLexiconStrictness, 'entries' => ['config_key' => ConfigLexiconEntry[]]] */
private array $configLexiconDetails = [];

public function __construct(
protected IDBConnection $connection,
Expand Down Expand Up @@ -706,6 +711,9 @@ private function getTypedValue(
ValueType $type,
): string {
$this->assertParams($userId, $app, $key);
if (!$this->matchAndApplyLexiconDefinition($app, $key, $lazy, $type, default: $default)) {
return $default; // returns default if strictness of lexicon is set to WARNING (block and report)
}
$this->loadConfig($userId, $lazy);

/**
Expand Down Expand Up @@ -1038,14 +1046,17 @@ private function setTypedValue(
ValueType $type,
): bool {
$this->assertParams($userId, $app, $key);
if (!$this->matchAndApplyLexiconDefinition($app, $key, $lazy, $type, $flags)) {
return false; // returns false as database is not updated
}
$this->loadConfig($userId, $lazy);

$inserted = $refreshCache = false;
$origValue = $value;
$sensitive = $this->isFlagged(self::FLAG_SENSITIVE, $flags);
if ($sensitive || ($this->hasKey($userId, $app, $key, $lazy) && $this->isSensitive($userId, $app, $key, $lazy))) {
$value = self::ENCRYPTION_PREFIX . $this->crypto->encrypt($value);
$flags |= UserConfig::FLAG_SENSITIVE;
$flags |= self::FLAG_SENSITIVE;
}

// if requested, we fill the 'indexed' field with current value
Expand Down Expand Up @@ -1803,4 +1814,98 @@ private function decryptSensitiveValue(string $userId, string $app, string $key,
]);
}
}

/**
* match and apply current use of config values with defined lexicon
*
* @throws UnknownKeyException
* @throws TypeConflictException
*/
private function matchAndApplyLexiconDefinition(
string $app,
string $key,
bool &$lazy,
ValueType &$type,
int &$flags = 0,
string &$default = '',
): bool {
$configDetails = $this->getConfigDetailsFromLexicon($app);
if (!array_key_exists($key, $configDetails['entries'])) {
return $this->applyLexiconStrictness($configDetails['strictness'], 'The user config key ' . $app . '/' . $key . ' is not defined in the config lexicon');
}

/** @var ConfigLexiconEntry $configValue */
$configValue = $configDetails['entries'][$key];
if ($type === ValueType::MIXED) {
$type = $configValue->getValueType(); // we overwrite if value was requested as mixed
} elseif ($configValue->getValueType() !== $type) {
throw new TypeConflictException('The user config key ' . $app . '/' . $key . ' is typed incorrectly in relation to the config lexicon');
}

$lazy = $configValue->isLazy();
$default = $configValue->getDefault() ?? $default; // default from Lexicon got priority
$flags = $configValue->getFlags();

if ($configValue->isDeprecated()) {
$this->logger->notice('User config key ' . $app . '/' . $key . ' is set as deprecated.');
}

return true;
}

/**
* manage ConfigLexicon behavior based on strictness set in IConfigLexicon
*
* @see IConfigLexicon::getStrictness()
* @param ConfigLexiconStrictness|null $strictness
* @param string $line
*
* @return bool TRUE if conflict can be fully ignored
* @throws UnknownKeyException
*/
private function applyLexiconStrictness(?ConfigLexiconStrictness $strictness, string $line = ''): bool {
if ($strictness === null) {
return true;
}

switch ($strictness) {
case ConfigLexiconStrictness::IGNORE:
return true;
case ConfigLexiconStrictness::NOTICE:
$this->logger->notice($line);
return true;
case ConfigLexiconStrictness::WARNING:
$this->logger->warning($line);
return false;
case ConfigLexiconStrictness::EXCEPTION:
throw new UnknownKeyException($line);
}

throw new UnknownKeyException($line);
}

/**
* extract details from registered $appId's config lexicon
*
* @param string $appId
*
* @return array{entries: array<array-key, ConfigLexiconEntry>, strictness: ConfigLexiconStrictness}
*/
private function getConfigDetailsFromLexicon(string $appId): array {
if (!array_key_exists($appId, $this->configLexiconDetails)) {
$entries = [];
$bootstrapCoordinator = \OCP\Server::get(Coordinator::class);
$configLexicon = $bootstrapCoordinator->getRegistrationContext()?->getConfigLexicon($appId);
foreach ($configLexicon?->getUserConfigs() ?? [] as $configEntry) {
$entries[$configEntry->getKey()] = $configEntry;
}

$this->configLexiconDetails[$appId] = [
'entries' => $entries,
'strictness' => $configLexicon?->getStrictness() ?? ConfigLexiconStrictness::IGNORE
];
}

return $this->configLexiconDetails[$appId];
}
}
Loading
Loading