diff --git a/build-logic/src/main/kotlin/capability-artifact.gradle.kts b/build-logic/src/main/kotlin/capability-artifact.gradle.kts new file mode 100644 index 000000000..9f705a047 --- /dev/null +++ b/build-logic/src/main/kotlin/capability-artifact.gradle.kts @@ -0,0 +1,21 @@ +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import org.gradle.jvm.tasks.Jar + +plugins { + id("capability") + id("relocations") +} + +val shadowJar = tasks.named("shadowJar") { + archiveClassifier.set("") +} + +tasks.named("jar").configure { + // Keep the normal Java/Maven artifact path, but let Shadow produce its relocated contents. + actions.clear() + dependsOn(shadowJar) +} + +tasks.named("assemble").configure { + dependsOn(shadowJar) +} diff --git a/build-logic/src/main/kotlin/capability.gradle.kts b/build-logic/src/main/kotlin/capability.gradle.kts new file mode 100644 index 000000000..1ef5e44d6 --- /dev/null +++ b/build-logic/src/main/kotlin/capability.gradle.kts @@ -0,0 +1,5 @@ +plugins { + id("api") +} + +group = "me.whereareiam.identica.capability" diff --git a/build-logic/src/main/kotlin/relocations.gradle.kts b/build-logic/src/main/kotlin/relocations.gradle.kts new file mode 100644 index 000000000..b92d8e915 --- /dev/null +++ b/build-logic/src/main/kotlin/relocations.gradle.kts @@ -0,0 +1,15 @@ +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + +plugins { + id("com.gradleup.shadow") +} + +tasks.withType().configureEach { + relocate("com.google.inject", "me.whereareiam.identica.library.guice") + relocate("com.google.common", "me.whereareiam.identica.library.guava") + relocate("org.bstats", "me.whereareiam.identica.library.bstats") + relocate("org.jdbi", "me.whereareiam.identica.library.jdbi") + relocate("me.whereareiam.dialectica", "me.whereareiam.identica.library.dialectica") + relocate("com.fasterxml.jackson", "me.whereareiam.identica.library.jackson") + relocate("org.yaml.snakeyaml", "me.whereareiam.identica.library.snakeyaml") +} diff --git a/build-logic/src/main/kotlin/runtime.gradle.kts b/build-logic/src/main/kotlin/runtime.gradle.kts index 98dca1507..c12ed07f2 100644 --- a/build-logic/src/main/kotlin/runtime.gradle.kts +++ b/build-logic/src/main/kotlin/runtime.gradle.kts @@ -3,20 +3,12 @@ import org.gradle.jvm.tasks.Jar plugins { id("shared") - id("com.gradleup.shadow") + id("relocations") } tasks.withType().configureEach { archiveBaseName.set(rootProject.name) - relocate("com.google.inject", "me.whereareiam.identica.library.guice") - relocate("com.google.common", "me.whereareiam.identica.library.guava") - relocate("org.bstats", "me.whereareiam.identica.library.bstats") - relocate("org.jdbi", "me.whereareiam.identica.library.jdbi") - relocate("me.whereareiam.dialectica", "me.whereareiam.identica.library.dialectica") - relocate("com.fasterxml.jackson", "me.whereareiam.identica.library.jackson") - relocate("org.yaml.snakeyaml", "me.whereareiam.identica.library.snakeyaml") - val defaultDestination = rootProject.layout.buildDirectory.dir("libs") if (providers.gradleProperty("output").isPresent) { diff --git a/build.gradle.kts b/build.gradle.kts index b981d5756..bd587662f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -17,6 +17,8 @@ allprojects { extensions.configure("attache") { transitive.set(true) + + mavenLocal() repository("https://maven.whereareiam.me/release") repository("https://maven.whereareiam.me/development") } @@ -35,3 +37,58 @@ tasks.register("pluginJars") { ":provider-premium-runtime:shadowJar" ) } + +val validatePipelineStructure = tasks.register("validatePipelineStructure") { + group = "verification" + description = "Validates pipeline package layout conventions." + + doLast { + val errors = mutableListOf() + val mainSources = fileTree(projectDir) { + include("identica-*/**/src/main/java/**/*.java") + exclude("**/build/**") + } + + mainSources.files.sortedBy { it.invariantSeparatorsPath }.forEach { file -> + val path = file.invariantSeparatorsPath + val text = file.readText() + + if ("/pipeline/scenario/base/" in path) { + errors += "Top-level scenario/base is forbidden: $path" + } + + if (path.contains("/pipeline/") && path.contains("/base/") && + !Regex("""\babstract\s+class\b""").containsMatchIn(text) + ) { + errors += "Only abstract classes may live in base packages: $path" + } + + val pipelineParticipantOutsidePipeline = + (path.contains("/identica-provider/") || path.contains("/identica-capability/")) && + !path.contains("/pipeline/") && + listOf( + "implements PipelineExtension", + "implements PipelinePhase<", + "extends InteractiveStep", + "extends SeamlessStep" + ).any(text::contains) + + if (pipelineParticipantOutsidePipeline) { + errors += "Pipeline-participating provider/capability class must live under pipeline/: $path" + } + } + + if (errors.isNotEmpty()) { + throw GradleException(buildString { + appendLine("Pipeline structure validation failed:") + errors.forEach { appendLine(" - $it") } + }) + } + } +} + +subprojects { + tasks.matching { it.name == "check" }.configureEach { + dependsOn(validatePipelineStructure) + } +} diff --git a/dev/scenarios/extended/bungeecord/proxy/plugins/Identica/providers/capabilities/recognition/settings.yml b/dev/scenarios/extended/bungeecord/proxy/plugins/Identica/providers/capabilities/recognition/settings.yml new file mode 100644 index 000000000..237a258f2 --- /dev/null +++ b/dev/scenarios/extended/bungeecord/proxy/plugins/Identica/providers/capabilities/recognition/settings.yml @@ -0,0 +1,4 @@ +enabled: true +eligibility: + untrustedIps: + enabled: false diff --git a/dev/scenarios/extended/bungeecord/proxy/plugins/Identica/providers/providers.yml b/dev/scenarios/extended/bungeecord/proxy/plugins/Identica/providers/providers.yml new file mode 100644 index 000000000..2fb5b1a09 --- /dev/null +++ b/dev/scenarios/extended/bungeecord/proxy/plugins/Identica/providers/providers.yml @@ -0,0 +1,17 @@ +providers: + - id: premium + capabilities: + restriction: + join: + enabled: true + allow: + - linked + - recognized + - id: credential + capabilities: + restriction: + join: + enabled: true + allow: + - linked + - recognized diff --git a/dev/scenarios/extended/bungeecord/proxy/plugins/Identica/settings.yml b/dev/scenarios/extended/bungeecord/proxy/plugins/Identica/settings.yml deleted file mode 100644 index d9824ae41..000000000 --- a/dev/scenarios/extended/bungeecord/proxy/plugins/Identica/settings.yml +++ /dev/null @@ -1,7 +0,0 @@ -level: 3 -sessions: - recognition: - enabled: true - eligibility: - untrustedIps: - enabled: false diff --git a/dev/scenarios/extended/velocity/proxy/plugins/identica/providers/capabilities/recognition/settings.yml b/dev/scenarios/extended/velocity/proxy/plugins/identica/providers/capabilities/recognition/settings.yml new file mode 100644 index 000000000..237a258f2 --- /dev/null +++ b/dev/scenarios/extended/velocity/proxy/plugins/identica/providers/capabilities/recognition/settings.yml @@ -0,0 +1,4 @@ +enabled: true +eligibility: + untrustedIps: + enabled: false diff --git a/dev/scenarios/extended/velocity/proxy/plugins/identica/providers/providers.yml b/dev/scenarios/extended/velocity/proxy/plugins/identica/providers/providers.yml new file mode 100644 index 000000000..2fb5b1a09 --- /dev/null +++ b/dev/scenarios/extended/velocity/proxy/plugins/identica/providers/providers.yml @@ -0,0 +1,17 @@ +providers: + - id: premium + capabilities: + restriction: + join: + enabled: true + allow: + - linked + - recognized + - id: credential + capabilities: + restriction: + join: + enabled: true + allow: + - linked + - recognized diff --git a/dev/scenarios/extended/velocity/proxy/plugins/identica/settings.yml b/dev/scenarios/extended/velocity/proxy/plugins/identica/settings.yml deleted file mode 100644 index d9824ae41..000000000 --- a/dev/scenarios/extended/velocity/proxy/plugins/identica/settings.yml +++ /dev/null @@ -1,7 +0,0 @@ -level: 3 -sessions: - recognition: - enabled: true - eligibility: - untrustedIps: - enabled: false diff --git a/dev/scenarios/normal/bungeecord/proxy/plugins/Identica/providers/capabilities/recognition/settings.yml b/dev/scenarios/normal/bungeecord/proxy/plugins/Identica/providers/capabilities/recognition/settings.yml new file mode 100644 index 000000000..237a258f2 --- /dev/null +++ b/dev/scenarios/normal/bungeecord/proxy/plugins/Identica/providers/capabilities/recognition/settings.yml @@ -0,0 +1,4 @@ +enabled: true +eligibility: + untrustedIps: + enabled: false diff --git a/dev/scenarios/normal/bungeecord/proxy/plugins/Identica/providers/providers.yml b/dev/scenarios/normal/bungeecord/proxy/plugins/Identica/providers/providers.yml new file mode 100644 index 000000000..2fb5b1a09 --- /dev/null +++ b/dev/scenarios/normal/bungeecord/proxy/plugins/Identica/providers/providers.yml @@ -0,0 +1,17 @@ +providers: + - id: premium + capabilities: + restriction: + join: + enabled: true + allow: + - linked + - recognized + - id: credential + capabilities: + restriction: + join: + enabled: true + allow: + - linked + - recognized diff --git a/dev/scenarios/normal/bungeecord/proxy/plugins/Identica/settings.yml b/dev/scenarios/normal/bungeecord/proxy/plugins/Identica/settings.yml deleted file mode 100644 index d9824ae41..000000000 --- a/dev/scenarios/normal/bungeecord/proxy/plugins/Identica/settings.yml +++ /dev/null @@ -1,7 +0,0 @@ -level: 3 -sessions: - recognition: - enabled: true - eligibility: - untrustedIps: - enabled: false diff --git a/dev/scenarios/normal/velocity/proxy/plugins/identica/providers/capabilities/recognition/settings.yml b/dev/scenarios/normal/velocity/proxy/plugins/identica/providers/capabilities/recognition/settings.yml new file mode 100644 index 000000000..237a258f2 --- /dev/null +++ b/dev/scenarios/normal/velocity/proxy/plugins/identica/providers/capabilities/recognition/settings.yml @@ -0,0 +1,4 @@ +enabled: true +eligibility: + untrustedIps: + enabled: false diff --git a/dev/scenarios/normal/velocity/proxy/plugins/identica/providers/providers.yml b/dev/scenarios/normal/velocity/proxy/plugins/identica/providers/providers.yml new file mode 100644 index 000000000..2fb5b1a09 --- /dev/null +++ b/dev/scenarios/normal/velocity/proxy/plugins/identica/providers/providers.yml @@ -0,0 +1,17 @@ +providers: + - id: premium + capabilities: + restriction: + join: + enabled: true + allow: + - linked + - recognized + - id: credential + capabilities: + restriction: + join: + enabled: true + allow: + - linked + - recognized diff --git a/dev/scenarios/normal/velocity/proxy/plugins/identica/settings.yml b/dev/scenarios/normal/velocity/proxy/plugins/identica/settings.yml deleted file mode 100644 index d9824ae41..000000000 --- a/dev/scenarios/normal/velocity/proxy/plugins/identica/settings.yml +++ /dev/null @@ -1,7 +0,0 @@ -level: 3 -sessions: - recognition: - enabled: true - eligibility: - untrustedIps: - enabled: false diff --git a/dev/scenarios/replication/bungeecord/proxy/plugins/Identica/providers/capabilities/recognition/settings.yml b/dev/scenarios/replication/bungeecord/proxy/plugins/Identica/providers/capabilities/recognition/settings.yml new file mode 100644 index 000000000..237a258f2 --- /dev/null +++ b/dev/scenarios/replication/bungeecord/proxy/plugins/Identica/providers/capabilities/recognition/settings.yml @@ -0,0 +1,4 @@ +enabled: true +eligibility: + untrustedIps: + enabled: false diff --git a/dev/scenarios/replication/bungeecord/proxy/plugins/Identica/providers/providers.yml b/dev/scenarios/replication/bungeecord/proxy/plugins/Identica/providers/providers.yml new file mode 100644 index 000000000..2fb5b1a09 --- /dev/null +++ b/dev/scenarios/replication/bungeecord/proxy/plugins/Identica/providers/providers.yml @@ -0,0 +1,17 @@ +providers: + - id: premium + capabilities: + restriction: + join: + enabled: true + allow: + - linked + - recognized + - id: credential + capabilities: + restriction: + join: + enabled: true + allow: + - linked + - recognized diff --git a/dev/scenarios/replication/bungeecord/proxy/plugins/Identica/settings.yml b/dev/scenarios/replication/bungeecord/proxy/plugins/Identica/settings.yml deleted file mode 100644 index d9824ae41..000000000 --- a/dev/scenarios/replication/bungeecord/proxy/plugins/Identica/settings.yml +++ /dev/null @@ -1,7 +0,0 @@ -level: 3 -sessions: - recognition: - enabled: true - eligibility: - untrustedIps: - enabled: false diff --git a/dev/scenarios/replication/velocity/proxy/plugins/identica/providers/capabilities/recognition/settings.yml b/dev/scenarios/replication/velocity/proxy/plugins/identica/providers/capabilities/recognition/settings.yml new file mode 100644 index 000000000..237a258f2 --- /dev/null +++ b/dev/scenarios/replication/velocity/proxy/plugins/identica/providers/capabilities/recognition/settings.yml @@ -0,0 +1,4 @@ +enabled: true +eligibility: + untrustedIps: + enabled: false diff --git a/dev/scenarios/replication/velocity/proxy/plugins/identica/providers/providers.yml b/dev/scenarios/replication/velocity/proxy/plugins/identica/providers/providers.yml new file mode 100644 index 000000000..2fb5b1a09 --- /dev/null +++ b/dev/scenarios/replication/velocity/proxy/plugins/identica/providers/providers.yml @@ -0,0 +1,17 @@ +providers: + - id: premium + capabilities: + restriction: + join: + enabled: true + allow: + - linked + - recognized + - id: credential + capabilities: + restriction: + join: + enabled: true + allow: + - linked + - recognized diff --git a/dev/scenarios/replication/velocity/proxy/plugins/identica/settings.yml b/dev/scenarios/replication/velocity/proxy/plugins/identica/settings.yml deleted file mode 100644 index d9824ae41..000000000 --- a/dev/scenarios/replication/velocity/proxy/plugins/identica/settings.yml +++ /dev/null @@ -1,7 +0,0 @@ -level: 3 -sessions: - recognition: - enabled: true - eligibility: - untrustedIps: - enabled: false diff --git a/docs/content/addons/community/index.md b/docs/content/addons/community/index.md new file mode 100644 index 000000000..fccf515a4 --- /dev/null +++ b/docs/content/addons/community/index.md @@ -0,0 +1,18 @@ +--- +title: Community Addons +description: Entry point for community-maintained addons. +--- + +Community addons are plugins made by the community and not maintained by Identica developers. +They extend Identica without acting as providers themselves. + +This page is the routing point for unofficial and community-maintained Identica addons. + +## Add an addon + +If you want a community addon to be listed here, request it through one of these channels: + +- [Discord](https://discord.arcadeya.com) +- [GitHub Issues or Pull Requests](https://github.com/whereareiam/Identica) + +Community addons can eventually be listed here as internal docs pages, external documentation links, or both. diff --git a/docs/content/addons/index.md b/docs/content/addons/index.md new file mode 100644 index 000000000..d37132c84 --- /dev/null +++ b/docs/content/addons/index.md @@ -0,0 +1,24 @@ +--- +title: Addons +description: Discover plugins that extend Identica without acting as providers. +--- + +Addons are plugins that extend Identica without acting as providers themselves. + +Identica has two addon groups: + +- official addons maintained by Identica developers +- community addons made by the community and not maintained by Identica developers + +Community addons may integrate with other plugins, change runtime behavior, or come with their own release and support expectations. +Review them carefully before you install them. + +## Addon discovery + +Use these pages to discover addon plugins: + +- [Official addons](./official/index.md) +- [Community addons](./community/index.md) + +Some addons may be documented directly in this docs site. +Others may link out to separate documentation maintained elsewhere. diff --git a/docs/content/addons/meta.json b/docs/content/addons/meta.json new file mode 100644 index 000000000..761308988 --- /dev/null +++ b/docs/content/addons/meta.json @@ -0,0 +1,8 @@ +{ + "title": "Addons", + "pages": [ + "index", + "official", + "community" + ] +} diff --git a/docs/content/addons/official/index.md b/docs/content/addons/official/index.md new file mode 100644 index 000000000..12e060080 --- /dev/null +++ b/docs/content/addons/official/index.md @@ -0,0 +1,6 @@ +--- +title: Official Addons +description: Addons maintained by Identica developers. +--- + +Official addons are plugins maintained by Identica developers that extend Identica without acting as providers. diff --git a/docs/content/configuration/conflicts/index.mdx b/docs/content/configuration/conflicts/index.mdx deleted file mode 100644 index 9054c6552..000000000 --- a/docs/content/configuration/conflicts/index.mdx +++ /dev/null @@ -1,311 +0,0 @@ ---- -title: Conflicts -description: Field-by-field explanation of Identica's conflict resolution configuration area. ---- - -The conflicts configuration controls how Identica resolves provider conflicts such as username collisions. - -Conflicts are configured per conflict key. -Each key defines: - -- a `default` rule -- optional provider-pair-specific `pairs` rules -- an ordered list of resolvers inside each rule - -## conflicts - -- Takes: a map keyed by conflict key -- Affects: how Identica resolves registered conflicts before allowing or denying progress - -Use this section when you want to change the resolver chain for a conflict type or define provider-specific behavior for a provider combination. - - - -```yaml -conflicts: - username: - default: - resolvers: - - id: "format_display" - parameters: - format: - pattern: "{username}*" - target: "joiner" - pairs: - - providers: - - "premium" - - "credential" - resolvers: - - id: "format_display" - parameters: - format: - pattern: "{username}_{incomingProvider:8}" - target: "joiner" -``` - - -```json -{ - "conflicts": { - "username": { - "default": { - "resolvers": [ - { - "id": "format_display", - "parameters": { - "format": { - "pattern": "{username}*" - }, - "target": "joiner" - } - } - ] - }, - "pairs": [ - { - "providers": ["premium", "credential"], - "resolvers": [ - { - "id": "format_display", - "parameters": { - "format": { - "pattern": "{username}_{incomingProvider:8}" - }, - "target": "joiner" - } - } - ] - } - ] - } - } -} -``` - - - -## Rule selection - -When Identica resolves a conflict for a key such as `username`, it evaluates rules in this order: - -1. matching provider-pair rules from `pairs` -2. the `default` rule - -Inside a rule, resolvers run in list order. -If a resolver returns `PASS`, Identica continues to the next resolver. -If every resolver passes, Identica falls back to the next applicable rule. -If no rule produces a result, Identica logs a warning and allows the conflict. - -## conflicts..default - -- Takes: a conflict rule object -- Affects: the fallback resolver chain for that conflict key - -The `default` rule runs when no provider-pair rule produces a non-pass result. - -## conflicts..pairs - -- Takes: a list of conflict rule objects -- Current default: `[]` -- Affects: provider-pair-specific conflict resolution before the default rule runs - -Each pair rule is matched when both provider ids are present in the conflict context, regardless of their order in the list. - -## conflicts..pairs[].providers - -- Takes: a list of provider ids -- Affects: which provider combination the pair rule applies to - -Most pair rules should contain exactly two provider ids such as `premium` and `credential`. - -## conflicts..default.force -## conflicts..pairs[].force - -Advanced - -- Takes: `true` or `false` -- Current default: `false` -- Affects: whether Identica should run a resolver even when it does not declare support for the conflict key - -Leave this disabled unless you intentionally want to bypass normal resolver compatibility checks. - -## conflicts..default.resolvers -## conflicts..pairs[].resolvers - -- Takes: a list of resolver entries -- Affects: the ordered resolver chain for the selected rule - -Each resolver entry selects a resolver type and provides its configuration parameters. - -## conflicts..default.resolvers[].id -## conflicts..pairs[].resolvers[].id - -- Takes: a string -- Affects: which registered conflict resolver is invoked - -Resolver ids must match a resolver registered by Identica core or an installed provider. - -## conflicts..default.resolvers[].parameters -## conflicts..pairs[].resolvers[].parameters - -- Takes: an object -- Current default: `{}` -- Affects: resolver-specific behavior - -The parameter structure depends on the selected resolver type. - -## Resolver types - -The sections above describe how conflict rules select and run resolvers. -The sections below describe the built-in resolver types themselves. - -### format_display - -`format_display` formats a replacement value from the current conflict context and returns that formatted value as the resolver result. - -#### conflicts..default.resolvers[].parameters.format.pattern -#### conflicts..pairs[].resolvers[].parameters.format.pattern - -- Takes: a string -- Affects: the formatted override value produced by `format_display` - -Supported placeholders: - -- `{username}` or `{requested}` -- `{incomingProvider}` and `{existingProvider}` -- `{incomingProviderId}` and `{existingProviderId}` -- `{random}` or `{random:4}` - -Provider display-name placeholders support optional truncation with `:maxSymbolCount`. - -Examples: - -```text -{incomingProvider} -{incomingProvider:8} -{existingProvider:6} -{username}_{incomingProvider:8}_{random:3} -``` - -Only `{incomingProvider}` and `{existingProvider}` support truncation. - -#### conflicts..default.resolvers[].parameters.format.uppercase -#### conflicts..pairs[].resolvers[].parameters.format.uppercase - -- Takes: `true` or `false` -- Current default: `false` -- Affects: whether the full formatted result is converted to uppercase - -#### conflicts..default.resolvers[].parameters.format.lowercase -#### conflicts..pairs[].resolvers[].parameters.format.lowercase - -- Takes: `true` or `false` -- Current default: `false` -- Affects: whether the full formatted result is converted to lowercase - -Avoid enabling both `uppercase` and `lowercase` at the same time. - -#### conflicts..default.resolvers[].parameters.target -#### conflicts..pairs[].resolvers[].parameters.target - -- Takes: `"joiner"`, `"existing"`, or `"both"` -- Current default: `"joiner"` -- Affects: which side receives the formatted override value - -Pattern with provider-name truncation: - - - -```yaml -resolvers: - - id: "format_display" - parameters: - format: - pattern: "{username}_{incomingProvider:8}" - target: "joiner" -``` - - -```json -{ - "resolvers": [ - { - "id": "format_display", - "parameters": { - "format": { - "pattern": "{username}_{incomingProvider:8}" - }, - "target": "joiner" - } - } - ] -} -``` - - - -Pattern with full-result uppercase formatting: - - - -```yaml -resolvers: - - id: "format_display" - parameters: - format: - pattern: "{username}_{incomingProvider}" - uppercase: true - target: "joiner" -``` - - -```json -{ - "resolvers": [ - { - "id": "format_display", - "parameters": { - "format": { - "pattern": "{username}_{incomingProvider}", - "uppercase": true - }, - "target": "joiner" - } - } - ] -} -``` - - - -Pattern applied to both sides: - - - -```yaml -resolvers: - - id: "format_display" - parameters: - format: - pattern: "{username}_{random:3}" - target: "both" -``` - - -```json -{ - "resolvers": [ - { - "id": "format_display", - "parameters": { - "format": { - "pattern": "{username}_{random:3}" - }, - "target": "both" - } - } - ] -} -``` - - diff --git a/docs/content/configuration/conflicts/meta.json b/docs/content/configuration/conflicts/meta.json deleted file mode 100644 index d6a746dbf..000000000 --- a/docs/content/configuration/conflicts/meta.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Conflicts" -} diff --git a/docs/content/configuration/features/index.mdx b/docs/content/configuration/features/index.mdx new file mode 100644 index 000000000..73edfa730 --- /dev/null +++ b/docs/content/configuration/features/index.mdx @@ -0,0 +1,10 @@ +--- +title: Features +description: Overview of feature-owned configuration areas and feature-scoped provider extensions. +--- + +Feature-owned configuration lives in this section when an installed feature contributes its own settings, documents, or nested provider overrides. + +## Current feature docs + +- [Verification](./verification/index.mdx) diff --git a/docs/content/configuration/features/meta.json b/docs/content/configuration/features/meta.json new file mode 100644 index 000000000..4712138ae --- /dev/null +++ b/docs/content/configuration/features/meta.json @@ -0,0 +1,7 @@ +{ + "title": "Features", + "pages": [ + "index", + "verification" + ] +} diff --git a/docs/content/configuration/features/verification/index.mdx b/docs/content/configuration/features/verification/index.mdx new file mode 100644 index 000000000..6018cd458 --- /dev/null +++ b/docs/content/configuration/features/verification/index.mdx @@ -0,0 +1,16 @@ +--- +title: Overview +description: Overview of configuration owned by Identica's verification feature. +--- + +Verification adds a second step that can be required after a provider already identifies the player. +In practice, this means a player first authenticates with a provider and then confirms an additional verification method before the flow can continue. +Verification methods can come from built-in functionality or from installed addons. + +## Verification in practice + +A player joins through the `credential` provider and enters their password successfully. +Instead of finishing login immediately, Identica asks for a code from the built-in default TOTP verification method. +The player enters the current code from their authenticator app, verification succeeds, and only then does the login flow complete. + +If verification is optional instead of required, the same provider can still work without forcing that second step for every player or every situation. diff --git a/docs/content/configuration/features/verification/meta.json b/docs/content/configuration/features/verification/meta.json new file mode 100644 index 000000000..8b1c8b2ec --- /dev/null +++ b/docs/content/configuration/features/verification/meta.json @@ -0,0 +1,7 @@ +{ + "title": "Verification", + "pages": [ + "index", + "settings" + ] +} diff --git a/docs/content/configuration/verification/index.mdx b/docs/content/configuration/features/verification/settings/index.mdx similarity index 61% rename from docs/content/configuration/verification/index.mdx rename to docs/content/configuration/features/verification/settings/index.mdx index 359c2a5f6..97e78e013 100644 --- a/docs/content/configuration/verification/index.mdx +++ b/docs/content/configuration/features/verification/settings/index.mdx @@ -1,14 +1,14 @@ --- -title: Verification -description: Field-by-field explanation of Identica's verification configuration area. +title: Settings +description: Field-by-field explanation of Identica's verification settings. --- -The verification configuration controls global verification timing and the built-in TOTP verification method. +This page provides the settings for verification and allows you to control its behavior. +You can also override the default behavior through provider overrides in `providers[].features.verification`. -Provider-specific rules such as whether verification is enabled, required, or selectable for a provider belong in the providers configuration. -This page is about the shared verification system itself. +## Settings -## challengeTtl +### challengeTtl - Takes: a positive duration - Current default: `"2m"` @@ -18,7 +18,7 @@ This TTL is used for pending challenge state such as a player being asked to ent If it is too short, players can lose their challenge before they finish it. If it is too long, stale pending challenge state remains valid longer than necessary. -## enrollmentTtl +### enrollmentTtl - Takes: a positive duration - Current default: `"10m"` @@ -26,7 +26,7 @@ If it is too long, stale pending challenge state remains valid longer than neces This TTL is used for enrollment state such as a player being partway through TOTP setup and confirmation. -## autoSelectCurrentProvider +### autoSelectCurrentProvider Advanced @@ -39,11 +39,11 @@ If no explicit provider id is present in the enrollment flow, Identica falls bac This reduces friction after successful enrollment, but some operators may prefer to require explicit method selection instead. -## totp +### totp The `totp` section configures the built-in authenticator-app verification method. -### displayName +#### displayName - Takes: a string - Current default: `"Authenticator App"` @@ -51,7 +51,7 @@ The `totp` section configures the built-in authenticator-app verification method If this is blank, the built-in method descriptor falls back to `totp`. -### issuer +#### issuer - Takes: a string - Current default: `"Identica"` @@ -59,7 +59,7 @@ If this is blank, the built-in method descriptor falls back to `totp`. This is what authenticator apps usually show as the service or account issuer when the QR code or OTPAuth URI is imported. -### labelFormat +#### labelFormat - Takes: a string - Current default: `"{player}@{providerId}"` @@ -72,7 +72,7 @@ The current TOTP setup step replaces these placeholders: This affects what the entry looks like in authenticator apps. -### digits +#### digits Advanced @@ -82,7 +82,7 @@ This affects what the entry looks like in authenticator apps. This value is used both when the OTPAuth URI is generated and when TOTP codes are later verified. -### period +#### period Advanced @@ -92,7 +92,7 @@ This value is used both when the OTPAuth URI is generated and when TOTP codes ar Internally, the built-in TOTP method converts this to seconds for OTPAuth generation and code verification. -### allowedPastWindows +#### allowedPastWindows Advanced @@ -102,7 +102,7 @@ Internally, the built-in TOTP method converts this to seconds for OTPAuth genera Higher values are more tolerant of clock drift and slow input, but they also make verification more permissive. -### allowedFutureWindows +#### allowedFutureWindows Advanced @@ -112,11 +112,11 @@ Higher values are more tolerant of clock drift and slow input, but they also mak Higher values can help with clock drift, but they also widen the accepted verification window. -## recoveryCodes +### recoveryCodes The `recoveryCodes` subsection controls backup codes generated during TOTP enrollment. -### enabled +#### enabled - Takes: `true` or `false` - Current default: `true` @@ -124,7 +124,7 @@ The `recoveryCodes` subsection controls backup codes generated during TOTP enrol If this is disabled, the built-in TOTP enrollment flow produces no recovery codes. -### amount +#### amount Advanced @@ -132,7 +132,7 @@ If this is disabled, the built-in TOTP enrollment flow produces no recovery code - Current default: `8` - Affects: how many recovery codes are generated during TOTP enrollment -### length +#### length Advanced @@ -140,7 +140,7 @@ If this is disabled, the built-in TOTP enrollment flow produces no recovery code - Current default: `10` - Affects: how many characters each recovery code contains before grouping is applied -### groupSize +#### groupSize Advanced @@ -149,3 +149,48 @@ If this is disabled, the built-in TOTP enrollment flow produces no recovery code - Affects: how recovery codes are visually grouped when generated This changes the formatting style of generated recovery codes, not whether they exist. + +## Provider overrides + +The verification feature contributes provider-scoped verification fields inside the providers configuration through `providers[].features.verification`. + + + Provider-scoped verification defaults are only applied when a provider declares a `features.verification` subtree. + In the current source, the official `credential` and `premium` providers then default to enabled verification with one enabled `totp` method. + + +### features.verification.enabled + +- Takes: `true` or `false` +- Affects: whether this provider participates in the shared verification system + +If the provider does not declare `features.verification` at all, it does not participate through this feature-owned subtree. + +### features.verification.required + +- Takes: `true` or `false` +- Current default: `false` +- Affects: whether verification becomes mandatory for this provider instead of optional + +### features.verification.unavailableSelectionPolicy + +- Takes: `"CLEAR_SELECTION"` or `"KEEP_LOCKED"` +- Current default: `KEEP_LOCKED` in the current declared defaults for official providers +- Affects: what happens when the selected verification method is unavailable + +### features.verification.methods + +- Takes: a list of method entries +- Affects: which verification methods can be used for this provider and in what order + +In the current declared defaults for the official `credential` and `premium` providers, this contains one enabled `totp` entry with priority `100`. + +Each method entry supports: + +- `id`: the verification method id, such as `totp` +- `enabled`: whether that method can be used +- `priority`: ordering when more than one method is available +- `required`: optional method-level override for provider-wide `required` +- `unavailableSelectionPolicy`: optional method-level override for provider-wide `unavailableSelectionPolicy` + +When a method entry leaves `required` or `unavailableSelectionPolicy` unset, the current resolver falls back to the provider-level verification settings. diff --git a/docs/content/configuration/index.mdx b/docs/content/configuration/index.mdx index 3fd81b516..110dea443 100644 --- a/docs/content/configuration/index.mdx +++ b/docs/content/configuration/index.mdx @@ -1,5 +1,5 @@ --- -title: Configuration +title: Overview description: Conceptual guide to Identica configuration areas and responsibilities. --- diff --git a/docs/content/configuration/meta.json b/docs/content/configuration/meta.json index 3d833656a..680510147 100644 --- a/docs/content/configuration/meta.json +++ b/docs/content/configuration/meta.json @@ -4,13 +4,12 @@ "index", "settings", "providers", + "features", "engine", "routing", "commands", - "conflicts", "messages", "persistence", - "replication", - "verification" + "replication" ] } diff --git a/docs/content/configuration/providers/capabilities/_partials/provider-overrides.mdx b/docs/content/configuration/providers/capabilities/_partials/provider-overrides.mdx new file mode 100644 index 000000000..6ec197518 --- /dev/null +++ b/docs/content/configuration/providers/capabilities/_partials/provider-overrides.mdx @@ -0,0 +1,16 @@ +Capability overrides are added manually inside a provider entry under `capabilities.`. +Identica does not create these nested override blocks automatically just because a provider supports a capability. + +If you want provider-specific behavior, add the capability block yourself. +If you leave the block out, the capability continues to use its shared settings. +If you add only some fields, only those fields are overridden and the rest continue using the shared capability configuration. + +Basic shape: + +```yaml +providers: + - id: "your-provider-id" + capabilities: + your_capability_id: + someField: value +``` diff --git a/docs/content/configuration/providers/capabilities/authoritative-username/index.mdx b/docs/content/configuration/providers/capabilities/authoritative-username/index.mdx new file mode 100644 index 000000000..b04bf17ed --- /dev/null +++ b/docs/content/configuration/providers/capabilities/authoritative-username/index.mdx @@ -0,0 +1,198 @@ +--- +title: Authoritative Username +description: Provider-controlled username synchronization behavior. +--- + +The `authoritative-username` capability lets a provider control the stored Identica username for an account. +When that provider reports a different username and the account is not protected by capability-owned manual authority state, Identica updates the stored username to match it. + +## Conflicts + +This capability contributes the built-in `username` conflict type. +The generic `conflicts` configuration page documents the shared rule structure. +This section documents the capability-specific selector and resolver behavior for `conflicts.username`. + +A username conflict happens when a provider with this capability wants to apply a username that is already used by another active account. +If no active conflicting account is found, normal username synchronization continues without conflict resolution. + +Before resolvers run, the capability also applies an entrypoint guard. +When the incoming account and the active conflicting account belong to different providers and both providers expose entrypoints, Identica requires that collision to be handled from a provider entrypoint flow. +If the flow did not come from an entrypoint, the conflict is denied with the capability's `entrypointRequired` message. + +### Example situation + +With the generated default conflict rules: + +- An active `credential` account is already using `Steve`. +- A `premium` account with this capability joins and also reports `Steve`. +- Identica detects a username conflict before applying the update. +- The `premium` + `credential` case rule runs first. +- That default case formats the incoming side as `{username}_{incomingProvider}`. +- In simple terms, the joining side gets a name like `Steve_Premium`. + +For other provider combinations that do not match a specific case rule, the generated default rule appends `*` to the incoming side instead, such as `Steve*`. + +The selector field below decides when a case rule applies. + +### conflicts.username.cases[].when.providers + +- Takes: a list of provider ids +- Affects: which provider combination a case rule applies to + +The rule matches when both provider ids are present in the conflict context, regardless of their order in the list. +Most rules should contain exactly two provider ids such as `premium` and `credential`. + +The resolver fields below configure the built-in `format_display` resolver for `conflicts.username`. + +### format_display + +`format_display` formats a replacement username from the current conflict context and returns that formatted value as the resolver result. + +#### conflicts.username.default.resolvers[].parameters.format.pattern +#### conflicts.username.cases[].resolvers[].parameters.format.pattern + +- Takes: a string +- Affects: the formatted username override produced by `format_display` + +Supported placeholders: + +- `{username}` or `{requested}` +- `{incomingProvider}` and `{existingProvider}` +- `{incomingProviderId}` and `{existingProviderId}` +- `{random}` or `{random:4}` + +Provider display-name placeholders support optional truncation with `:maxSymbolCount`. + +Examples: + +```text +{incomingProvider} +{incomingProvider:8} +{existingProvider:6} +{username}_{incomingProvider:8}_{random:3} +``` + +Only `{incomingProvider}` and `{existingProvider}` support truncation. + +#### conflicts.username.default.resolvers[].parameters.format.uppercase +#### conflicts.username.cases[].resolvers[].parameters.format.uppercase + +- Takes: `true` or `false` +- Current default: `false` +- Affects: whether the full formatted result is converted to uppercase + +#### conflicts.username.default.resolvers[].parameters.format.lowercase +#### conflicts.username.cases[].resolvers[].parameters.format.lowercase + +- Takes: `true` or `false` +- Current default: `false` +- Affects: whether the full formatted result is converted to lowercase + +Avoid enabling both `uppercase` and `lowercase` at the same time. + +#### conflicts.username.default.resolvers[].parameters.target +#### conflicts.username.cases[].resolvers[].parameters.target + +- Takes: `"joiner"`, `"existing"`, or `"both"` +- Current default: `"joiner"` +- Affects: which side receives the formatted override value + +`joiner` means the incoming account side. + +Pattern with provider-name truncation: + + + +```yaml +resolvers: + - id: "format_display" + parameters: + format: + pattern: "{username}_{incomingProvider:8}" + target: "joiner" +``` + + +```json +{ + "resolvers": [ + { + "id": "format_display", + "parameters": { + "format": { + "pattern": "{username}_{incomingProvider:8}" + }, + "target": "joiner" + } + } + ] +} +``` + + + +Pattern with full-result uppercase formatting: + + + +```yaml +resolvers: + - id: "format_display" + parameters: + format: + pattern: "{username}_{incomingProvider}" + uppercase: true + target: "joiner" +``` + + +```json +{ + "resolvers": [ + { + "id": "format_display", + "parameters": { + "format": { + "pattern": "{username}_{incomingProvider}", + "uppercase": true + }, + "target": "joiner" + } + } + ] +} +``` + + + +Pattern applied to both sides: + + + +```yaml +resolvers: + - id: "format_display" + parameters: + format: + pattern: "{username}_{random:3}" + target: "both" +``` + + +```json +{ + "resolvers": [ + { + "id": "format_display", + "parameters": { + "format": { + "pattern": "{username}_{random:3}" + }, + "target": "both" + } + } + ] +} +``` + + diff --git a/docs/content/configuration/providers/capabilities/index.md b/docs/content/configuration/providers/capabilities/index.md new file mode 100644 index 000000000..6a4571177 --- /dev/null +++ b/docs/content/configuration/providers/capabilities/index.md @@ -0,0 +1,22 @@ +--- +title: Capabilities +description: Shared and provider-scoped configuration for built-in provider capabilities. +--- + +Capabilities are optional behavior modules that providers can advertise and install. +They are not automatic core behavior for every provider. + +This section documents the built-in/default capabilities that ship with Identica. + +Capabilities commonly surface configuration in two operator-facing places: + +- shared capability files under `providers/capabilities//...` +- provider-scoped fields under `providers[].capabilities....` + +Only providers that advertise a capability can use that capability's provider-scoped fields. + +## Documented capabilities + +- [Authoritative Username](./authoritative-username/index.mdx) +- [Restriction](./restriction/index.mdx) +- [Recognition](./recognition/index.mdx) diff --git a/docs/content/configuration/providers/capabilities/meta.json b/docs/content/configuration/providers/capabilities/meta.json new file mode 100644 index 000000000..b09fc4d84 --- /dev/null +++ b/docs/content/configuration/providers/capabilities/meta.json @@ -0,0 +1,9 @@ +{ + "title": "Capabilities", + "pages": [ + "index", + "authoritative-username", + "recognition", + "restriction" + ] +} diff --git a/docs/content/configuration/providers/capabilities/recognition/index.mdx b/docs/content/configuration/providers/capabilities/recognition/index.mdx new file mode 100644 index 000000000..d369aa438 --- /dev/null +++ b/docs/content/configuration/providers/capabilities/recognition/index.mdx @@ -0,0 +1,128 @@ +--- +title: Recognition +description: Shared and provider-scoped configuration for the built-in recognition capability. +--- + +The `recognition` capability adds optional reconnect recognition behavior for providers that advertise it. +When recognition is enabled and the configured signals still match recent saved state, a reconnecting player can be recognized without going through the full provider flow again. + +In simple terms, recognition is a way to skip the normal authentication step for a player that Identica believes it already knows. +Instead of asking that player to authenticate again, Identica tries to resolve them automatically from recent saved recognition state and the configured signals. + +This can be convenient, but it can also be misused in the wrong environment or with weak configuration. +If several players can appear to share the same network identity or signal set, recognition can become easier to exploit than a normal explicit authentication step. + +The built-in recognition capability uses one shared capability file and provider-scoped capability fields. + +## Settings + +Shared recognition settings live under `providers/capabilities/recognition/settings`. + +### enabled + +Warning + +- Takes: `true` or `false` +- Current default: `false` +- Affects: whether recognition is allowed to run at all + +Recognition is disabled by default. +Enable it only when you understand the trust and network-identity tradeoffs in your environment. + +### validity + +Advanced + +- Takes: a positive duration +- Current default: `"12h"` +- Affects: how long a previously authenticated player can still be recognized on reconnect + +Once this duration expires, the player simply goes through the normal provider flow again. + +### window + +Advanced + +- Takes: a positive duration +- Current default: `"10m"` +- Affects: how long recognized-connection markers stay cached + +### defaultSignals + +Warning + +- Takes: a list containing any of `"USERNAME"`, `"IP"`, `"VIRTUAL_HOST"` +- Current default: `["USERNAME", "IP", "VIRTUAL_HOST"]` +- Affects: the signal set used when a provider does not override its own recognition signals + +### eligibility.untrustedIps.enabled + +Warning + +- Takes: `true` or `false` +- Current default: `true` +- Affects: whether configured client IPs and CIDR ranges make recognition ineligible + +### eligibility.untrustedIps.entries + +Warning + +- Takes: a list of exact IP literals or IPv4/IPv6 CIDR ranges +- Current default: `["127.0.0.1", "::1", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"]` +- Affects: which client IPs make recognition ineligible + +The generated defaults target the common proxy-misconfiguration case where Identica sees loopback or private addresses instead of the real client IP. + +### replication.snapshotNamespace + +Advanced + +- Takes: a string namespace +- Current default: `"identica:session-recognition:snapshot"` +- Affects: where recognition snapshots are stored in the replication-backed cache layer + +### replication.recognizedConnectionNamespace + +Advanced + +- Takes: a string namespace +- Current default: `"identica:recognized-connection"` +- Affects: where recognized-connection markers are stored in the replication-backed cache layer + +## Provider Overrides + +Provider-specific recognition fields live under `providers[].capabilities.recognition`. +They only apply to providers that advertise the `recognition` capability. + + + +### capabilities.recognition.enabled + +Warning + +- Takes: `true` or `false` +- Current default: absent +- Affects: whether this provider inherits or overrides the shared `recognition.enabled` value + +When this field is absent, the provider uses the shared capability setting. + +### capabilities.recognition.signals + +Warning + +- Takes: a list containing any of `"USERNAME"`, `"IP"`, `"VIRTUAL_HOST"` +- Current default: absent +- Affects: the signal set used by this provider when it evaluates recognition + +When this field is absent or empty, the provider uses `recognition.defaultSignals`. + +### capabilities.recognition.allowOnUntrustedIps + +Advanced + +- Takes: `true` or `false` +- Current default: `false` +- Affects: whether this provider stays recognition-eligible from client IPs blocked by `recognition.eligibility.untrustedIps` + +Leave this `false` for the normal safety posture. +Set it to `true` only when this specific provider should remain recognition-eligible on IPs that are otherwise blocked. diff --git a/docs/content/configuration/providers/capabilities/restriction/index.mdx b/docs/content/configuration/providers/capabilities/restriction/index.mdx new file mode 100644 index 000000000..f07351272 --- /dev/null +++ b/docs/content/configuration/providers/capabilities/restriction/index.mdx @@ -0,0 +1,15 @@ +--- +title: Restriction +description: Shared configuration and extension points for the generic restriction capability. +--- + +The `restriction` capability provides the generic framework for provider restriction types. +It does not define the behavior of every restriction itself. + +Shared restriction settings live under `providers/capabilities/restriction/settings`. + +Provider-specific restriction overrides live under `providers[].capabilities.restriction...`. + +## Typed modules + +- [Join Restriction](./join/index.mdx) diff --git a/docs/content/configuration/providers/capabilities/restriction/join/index.mdx b/docs/content/configuration/providers/capabilities/restriction/join/index.mdx new file mode 100644 index 000000000..0cf1b2790 --- /dev/null +++ b/docs/content/configuration/providers/capabilities/restriction/join/index.mdx @@ -0,0 +1,27 @@ +--- +title: Join Restriction +description: Provider-scoped configuration for the typed join restriction module. +--- + +The `join` typed restriction module is installed under the generic `restriction` capability. +It controls whether a provider may temporarily restrict new joins through the admin restriction commands. + +Join restriction provider overrides live under `providers[].capabilities.restriction.join`. + +## Provider Overrides + +### capabilities.restriction.join.enabled + +Advanced + +- Takes: `true` or `false` +- Current default: `false` +- Affects: whether this provider exposes the typed join restriction runtime + +### capabilities.restriction.join.allow + +Advanced + +- Takes: a list of signal ids +- Current default: absent unless explicitly configured +- Affects: which contributed join signals may allow the provider while join restriction is active diff --git a/docs/content/configuration/providers/capabilities/restriction/meta.json b/docs/content/configuration/providers/capabilities/restriction/meta.json new file mode 100644 index 000000000..ad880dc29 --- /dev/null +++ b/docs/content/configuration/providers/capabilities/restriction/meta.json @@ -0,0 +1,7 @@ +{ + "title": "Restriction", + "pages": [ + "index", + "join" + ] +} diff --git a/docs/content/configuration/providers/conflicts.mdx b/docs/content/configuration/providers/conflicts.mdx new file mode 100644 index 000000000..cc6b2fd30 --- /dev/null +++ b/docs/content/configuration/providers/conflicts.mdx @@ -0,0 +1,164 @@ +--- +title: Conflicts +description: Field-by-field explanation of Identica's conflict resolution configuration area. +--- + +The conflicts configuration controls how Identica resolves provider conflicts such as username collisions. + +Conflicts are configured per conflict key. +Each key defines: + +- a `default` rule +- optional ordered `cases` rules +- an ordered list of resolvers inside each rule + +## conflicts + +- Takes: a map keyed by conflict key +- Affects: how Identica resolves registered conflicts before allowing or denying progress + +Use this section when you want to change the resolver chain for a conflict type or define type-specific behavior for a matching rule selector. + + + +```yaml +conflicts: + username: + default: + resolvers: + - id: "format_display" + parameters: + format: + pattern: "{username}*" + target: "joiner" + cases: + - when: + providers: + - "premium" + - "credential" + resolvers: + - id: "format_display" + parameters: + format: + pattern: "{username}_{incomingProvider:8}" + target: "joiner" +``` + + +```json +{ + "conflicts": { + "username": { + "default": { + "resolvers": [ + { + "id": "format_display", + "parameters": { + "format": { + "pattern": "{username}*" + }, + "target": "joiner" + } + } + ] + }, + "cases": [ + { + "when": { + "providers": ["premium", "credential"] + }, + "resolvers": [ + { + "id": "format_display", + "parameters": { + "format": { + "pattern": "{username}_{incomingProvider:8}" + }, + "target": "joiner" + } + } + ] + } + ] + } + } +} +``` + + + +## Rule selection + +When Identica resolves a conflict for a key such as `username`, it evaluates rules in this order: + +1. matching rules from `cases` +2. the `default` rule + +Inside a rule, resolvers run in list order. +If a resolver returns `PASS`, Identica continues to the next resolver. +If every resolver passes, Identica falls back to the next applicable rule. +If no rule produces a result, Identica logs a warning and allows the conflict. + +Rule matching is owned by the contributed conflict type for that key. +Core stores the opaque `when` object but does not interpret it. + +## conflicts.[key].default + +- Takes: a conflict rule object +- Affects: the fallback resolver chain for that conflict key + +The `default` rule runs when no matching case rule produces a non-pass result. + +## conflicts.[key].cases + +- Takes: a list of conflict rule objects +- Current default: `[]` +- Affects: type-owned conflict resolution before the default rule runs + +Each case rule is checked in list order. +The contributed conflict type decides whether the rule's `when` object matches the current conflict context. + +## conflicts.[key].cases[].when + +- Takes: an object +- Current default: `{}` +- Affects: which contexts the rule applies to, according to the contributed conflict type + +The shape and meaning of `when` is owned by the contributed conflict type for that key. + +## conflicts.[key].default.force +## conflicts.[key].cases[].force + +Advanced + +- Takes: `true` or `false` +- Current default: `false` +- Affects: whether Identica should run a resolver even when it does not declare support for the conflict key + +Leave this disabled unless you intentionally want to bypass normal resolver compatibility checks. + +## conflicts.[key].default.resolvers +## conflicts.[key].cases[].resolvers + +- Takes: a list of resolver entries +- Affects: the ordered resolver chain for the selected rule + +Each resolver entry selects a resolver type and provides its configuration parameters. + +## conflicts.[key].default.resolvers[].id +## conflicts.[key].cases[].resolvers[].id + +- Takes: a string +- Affects: which registered conflict resolver is invoked + +Resolver ids must match a resolver registered by a contributed conflict type or another installed module. + +## conflicts.[key].default.resolvers[].parameters +## conflicts.[key].cases[].resolvers[].parameters + +- Takes: an object +- Current default: `{}` +- Affects: resolver-specific behavior + +The parameter structure depends on the selected resolver type. +Resolver ids, parameter schemas, and type-specific `when` selectors should be documented by the module that contributes them. diff --git a/docs/content/configuration/providers/index.mdx b/docs/content/configuration/providers/index.mdx index e9ed0a5aa..4be0a5bc9 100644 --- a/docs/content/configuration/providers/index.mdx +++ b/docs/content/configuration/providers/index.mdx @@ -4,7 +4,10 @@ description: Field-by-field explanation of Identica's provider configuration are --- The providers configuration controls which identity providers are enabled and how provider-specific behavior is applied. -Use this file to enable providers, set priority and entrypoints, configure restrictions and verification, and tune provider-specific session and recognition behavior. +Use this file to enable providers, set priority and entrypoints, tune provider-specific session behavior, and apply capability-owned and feature-owned provider fields. + +Capability-specific shared fields and provider-scoped capability overrides are documented under [Capabilities](./capabilities/index.md). +Conflict configuration for provider-owned conflicts is documented under [Conflicts](./conflicts.mdx). ## behavior @@ -22,32 +25,18 @@ Identica uses this when it marks that a provider attempt already happened for a Longer values make repeated attempts stay blocked longer. Shorter values let retries happen sooner. -### joinRestrictionToggleTtl - -Advanced - -- Takes: a positive duration -- Current default: `"365d"` -- Affects: how long provider join restriction runtime toggles stay active in the replicated toggle store - -This TTL is used when join restrictions are enabled at runtime. -Longer values make administrative toggles persist for longer without needing to be reapplied. -Shorter values let temporary restrictions expire automatically sooner. - ```yaml behavior: attemptTtl: "10m" - joinRestrictionToggleTtl: "365d" ``` ```json { "behavior": { - "attemptTtl": "10m", - "joinRestrictionToggleTtl": "365d" + "attemptTtl": "10m" } } ``` @@ -57,7 +46,7 @@ behavior: ## providers - Takes: a list of provider entries -- Affects: provider enablement, ordering, entrypoint routing, verification policy, and provider-specific session and recognition behavior +- Affects: provider enablement, ordering, entrypoint routing, provider-specific session behavior, capability and feature-owned provider overrides Provider ids should match installed provider descriptors, such as `premium` or `credential`. @@ -73,25 +62,6 @@ providers: - "premium.example.com" session: concurrencyPolicy: "REJECT_NEW" - recognition: - enabled: true - signals: - - "USERNAME" - - "VIRTUAL_HOST" - allowOnUntrustedIps: false - restriction: - join: - enabled: false - allow: - - "RECOGNIZED" - verification: - enabled: true - required: false - unavailableSelectionPolicy: "KEEP_LOCKED" - methods: - - id: "totp" - enabled: true - priority: 100 ``` @@ -105,30 +75,7 @@ providers: "priority": 100, "entrypoints": ["premium.example.com"], "session": { - "concurrencyPolicy": "REJECT_NEW", - "recognition": { - "enabled": true, - "signals": ["USERNAME", "VIRTUAL_HOST"], - "allowOnUntrustedIps": false - } - }, - "restriction": { - "join": { - "enabled": false, - "allow": ["RECOGNIZED"] - } - }, - "verification": { - "enabled": true, - "required": false, - "unavailableSelectionPolicy": "KEEP_LOCKED", - "methods": [ - { - "id": "totp", - "enabled": true, - "priority": 100 - } - ] + "concurrencyPolicy": "REJECT_NEW" } } ] @@ -186,114 +133,31 @@ Use `session` when one provider should behave differently from the global live-s If this is absent, Identica uses `settings.sessions.concurrencyPolicy`. -#### session.recognition +### capabilities -- Takes: an object -- Current default: `{}` for providers without recognition-specific changes -- Affects: provider-specific reconnect recognition behavior +Use `capabilities` for provider-scoped fields owned by installed capabilities. +Only providers that advertise a capability can use that capability's fields. -Use `session.recognition` when one provider should evaluate reconnect recognition differently from the global defaults. - -##### session.recognition.enabled - -Warning - -- Takes: `true` or `false` +- Takes: an object keyed by capability id - Current default: absent -- Affects: whether this provider inherits or overrides `settings.sessions.recognition.enabled` - -When this field is absent, the provider inherits `settings.sessions.recognition.enabled`. +- Affects: provider-scoped overrides contributed by capabilities -##### session.recognition.signals +The shape inside `capabilities` is not global. +Each installed capability defines its own nested fields. -Warning + -- Takes: a list containing any of `"USERNAME"`, `"IP"`, `"VIRTUAL_HOST"` -- Current default: absent -- Affects: the signal set used when this provider evaluates reconnect recognition +### features -When this field is absent or empty, the provider inherits `settings.sessions.recognition.defaultSignals`. +Use `features` for provider-scoped fields owned by installed features. +Only installed features that extend provider configuration can use nested fields here. -##### session.recognition.allowOnUntrustedIps - -Advanced - -- Takes: `true` or `false` -- Current default: `false` -- Affects: whether this provider stays recognition-eligible from client IPs blocked by `settings.sessions.recognition.eligibility.untrustedIps` - -Leave this `false` for the normal safety posture. -Set it to `true` only when this specific provider should remain recognition-eligible on client IPs that are otherwise blocked. - -### restriction - -Use `restriction` for provider-specific restriction policies. -Right now it contains `restriction.join`, and the extra nesting leaves room for more restriction types later without flattening the provider entry further. - -#### restriction.join - -Use `restriction.join` when a provider should support runtime join restriction toggles through the admin provider restriction commands. - -##### restriction.join.enabled - -Advanced - -- Takes: `true` or `false` -- Current default: `false` -- Affects: whether this provider exposes a runtime-toggleable join restriction policy - -When this is `false`, `/identica admin provider restriction enable ` will fail for that provider. -When this is `true`, the configured allow-list can be activated at runtime by command. - -##### restriction.join.allow - -Advanced - -- Takes: a list containing `"RECOGNIZED"` and/or `"LINKED"` -- Current default: absent unless explicitly configured -- Affects: what the runtime provider restriction does when it is active - -Available conditions: - -- `RECOGNIZED`: only reconnect-recognized players may continue through this provider -- `LINKED`: already-linked players may continue through this provider - -When the list contains both values, players may continue when either condition matches. -When the list is empty, all new joins through the provider are denied. - -### verification - -Use `verification` when a provider should participate in the shared verification system or override its provider-level verification behavior. - -#### verification.enabled - -- Takes: `true` or `false` -- Current default: `false` unless the provider entry explicitly enables it -- Affects: whether this provider participates in the shared verification system - -#### verification.required - -- Takes: `true` or `false` -- Current default: `false` -- Affects: whether verification becomes mandatory for this provider instead of optional - -#### verification.unavailableSelectionPolicy - -- Takes: `"CLEAR_SELECTION"` or `"KEEP_LOCKED"` -- Current default: `CLEAR_SELECTION` when unset in current source -- Affects: what happens when the selected verification method is unavailable - -#### verification.methods - -- Takes: a list of method entries -- Affects: which verification methods can be used for this provider and in what order - -Each method entry supports: +- Takes: an object keyed by feature id +- Current default: absent +- Affects: provider-scoped overrides contributed by features -- `id`: the verification method id, such as `totp` -- `enabled`: whether that method can be used -- `priority`: ordering when more than one method is available -- `required`: optional method-level override for provider-wide `verification.required` -- `unavailableSelectionPolicy`: optional method-level override for provider-wide `verification.unavailableSelectionPolicy` +The shape inside `features` is not global. +Each installed feature defines its own nested fields. -When a method entry leaves `required` or `unavailableSelectionPolicy` unset, the current resolver falls back to the provider-level verification settings. +The official verification feature contributes `verification` here. +See [Verification settings](../features/verification/settings/index.mdx#provider-overrides). diff --git a/docs/content/configuration/providers/meta.json b/docs/content/configuration/providers/meta.json new file mode 100644 index 000000000..0203c576a --- /dev/null +++ b/docs/content/configuration/providers/meta.json @@ -0,0 +1,8 @@ +{ + "title": "Providers", + "pages": [ + "index", + "capabilities", + "conflicts" + ] +} diff --git a/docs/content/configuration/routing/index.mdx b/docs/content/configuration/routing/index.mdx index d0107fae7..e3a46b7fd 100644 --- a/docs/content/configuration/routing/index.mdx +++ b/docs/content/configuration/routing/index.mdx @@ -1,4 +1,4 @@ - --- +--- title: Routing description: Field-by-field explanation of Identica's routing configuration area. --- diff --git a/docs/content/configuration/settings/index.mdx b/docs/content/configuration/settings/index.mdx index f31cbc440..8863a97c8 100644 --- a/docs/content/configuration/settings/index.mdx +++ b/docs/content/configuration/settings/index.mdx @@ -119,13 +119,11 @@ identity: ## sessions -`sessions` controls live-session behavior and optional reconnect recognition. +`sessions` controls live-session behavior for currently connected players. - live session behavior for currently connected players -- optional reconnect recognition for known players A session exists while a player is online. -Recognition is a separate optional shortcut that can let a reconnecting player skip full provider authentication when the configured signals match a previously saved recognition state that is still valid. ### concurrencyPolicy @@ -156,94 +154,12 @@ Identica closes the logical session immediately on disconnect. This is not an offline remember-me duration. It only controls the backing cache survival time for active sessions. -### recognition.enabled - -Warning - -- Takes: `true` or `false` -- Current default: `false` -- Affects: the default reconnect-recognition policy inherited by providers that do not override it - -Recognition is disabled by default. -It can make reconnects easier, but it relies on matching signals such as username, IP, and virtual host after the provider subject is already known. - -This is risky in environments where several players can appear to share the same network identity. -That includes NAT, carrier-grade NAT, shared Wi-Fi, VPN exits, reverse proxies, and hosting setups where different users can present the same IP path. - -Enable recognition only if you understand that tradeoff and accept the risk for your environment. - -### recognition.validity - -Advanced - -- Takes: a positive duration -- Current default: `"12h"` -- Affects: how long a previously authenticated player can still be recognized on reconnect - -This duration belongs to recognition, not to the live-session lifetime. -Once it expires, the player simply goes through normal provider authentication on the next reconnect. - -### recognition.defaultSignals - -Warning - -- Takes: a list containing any of `"USERNAME"`, `"IP"`, `"VIRTUAL_HOST"` -- Current default: `["USERNAME", "IP", "VIRTUAL_HOST"]` -- Affects: the signal set inherited by providers that do not override their own recognition signals - -### recognition.eligibility.untrustedIps.enabled - -Warning - -- Takes: `true` or `false` -- Current default: `true` -- Affects: whether recognition remains eligible for configured client IPs and CIDR ranges - -This rule controls whether recognition may run at all, including reconnect recognition and provider-scoped handshake recognition. - -Use this when reconnect recognition is generally acceptable, but there are client networks you do not trust for automatic recognition decisions. -The generated defaults target the common “proxy forwarded a loopback/private proxy IP instead of the real client IP” failure mode. - -### recognition.eligibility.untrustedIps.entries - -Warning - -- Takes: a list of exact IP literals or IPv4/IPv6 CIDR ranges -- Current default: `["127.0.0.1", "::1", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"]` -- Affects: which client IPs make recognition ineligible - -The generated defaults cover loopback and RFC1918 private ranges, which are often a sign that Identica is behind a proxy that is forwarding the wrong source IP. - -Entries must be one of: - -- an exact IP such as `127.0.0.1` -- an IPv4 CIDR such as `192.168.0.0/16` -- an IPv6 CIDR such as `2001:db8::/32` - -Malformed entries fail loudly during config use with a settings-path error that points to the bad list index. - ```yaml sessions: concurrencyPolicy: "REPLACE_EXISTING" activeTtl: "12h" - recognition: - enabled: false - validity: "12h" - defaultSignals: - - "USERNAME" - - "IP" - - "VIRTUAL_HOST" - eligibility: - untrustedIps: - enabled: true - entries: - - "127.0.0.1" - - "::1" - - "10.0.0.0/8" - - "172.16.0.0/12" - - "192.168.0.0/16" ``` @@ -251,18 +167,7 @@ sessions: { "sessions": { "concurrencyPolicy": "REPLACE_EXISTING", - "activeTtl": "12h", - "recognition": { - "enabled": false, - "validity": "12h", - "defaultSignals": ["USERNAME", "IP", "VIRTUAL_HOST"], - "eligibility": { - "untrustedIps": { - "enabled": true, - "entries": ["127.0.0.1", "::1", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"] - } - } - } + "activeTtl": "12h" } } ``` diff --git a/docs/content/developer/advanced/meta.json b/docs/content/developer/advanced/meta.json index 8650e28ec..10dda70a9 100644 --- a/docs/content/developer/advanced/meta.json +++ b/docs/content/developer/advanced/meta.json @@ -1,6 +1,7 @@ { "title": "Advanced", "pages": [ + "pipeline-layout", "events" ] } diff --git a/docs/content/faq/index.mdx b/docs/content/faq/index.mdx new file mode 100644 index 000000000..6087dcad7 --- /dev/null +++ b/docs/content/faq/index.mdx @@ -0,0 +1,40 @@ +--- +title: FAQ +description: Short answers to common questions about Identica. +--- + +## General + + + + Identica does not currently ship official migrators for other authentication plugins. + + If you want to migrate anyway, ask in [Discord](https://discord.arcadeya.com). + We may be able to provide a temporary Python migration script that you can review and run locally against your own data. + + + +## Flow + + + + Identica is an identity plugin. + It does not handle movement restrictions, block interaction, or general gameplay isolation during registration, authentication, or migration. + + Use a separate environment that blocks those actions while the player is in the Identica flow. + We recommend [PicoLimbo](https://picolimbo.quozul.dev/about/introduction.html), because it can host virtual servers inside your proxy and does not require separate full backend servers. + + + + This is already warned about in the installation guides. + You should block commands on your proxy, or use a community addon for that purpose. + + [Identica CommandBlocker](https://modrinth.com/plugin/identica-commandblocker) can automatically block unrelated commands and allow only the ones needed during the Identica flow. + + + + That should usually be handled outside Identica itself by the rest of your network setup. + + You can configure your other plugins so those messages are only sent to the servers where they belong, or use a community addon such as [Identica ChatBlocker](https://modrinth.com/plugin/identica-chatblocker). + + diff --git a/docs/content/installation/_partials/platform-first-boot.mdx b/docs/content/installation/_partials/platform-first-boot.mdx index fb164ddc7..d8d985cd6 100644 --- a/docs/content/installation/_partials/platform-first-boot.mdx +++ b/docs/content/installation/_partials/platform-first-boot.mdx @@ -9,14 +9,13 @@ - `plugins/identica/routing` - `plugins/identica/messages` - `plugins/identica/commands` - - `plugins/identica/verification` + - `plugins/identica/features/verification/settings` - `plugins/identica/persistence` - `plugins/identica/replication` - `plugins/identica/providers/conflicts` + - `plugins/identica/providers/capabilities` - `plugins/identica/providers/providers` - Provider-specific config directories for enabled providers also appear under `plugins/identica/providers/`. - The current default boot path creates a configuration marker with `type=YAML`. It is safe to stop the proxy after these files exist. @@ -31,14 +30,13 @@ - `plugins/identica/routing` - `plugins/identica/messages` - `plugins/identica/commands` - - `plugins/identica/verification` + - `plugins/identica/features/verification/settings` - `plugins/identica/persistence` - `plugins/identica/replication` - `plugins/identica/providers/conflicts` + - `plugins/identica/providers/capabilities` - `plugins/identica/providers/providers` - Provider-specific config directories for enabled providers also appear under `plugins/identica/providers/`. - The current default boot path creates a configuration marker with `type=YAML`. It is safe to stop the proxy after these files exist. diff --git a/docs/content/installation/_partials/provider-ordering.mdx b/docs/content/installation/_partials/provider-ordering.mdx index ee3a49bde..bdd9af494 100644 --- a/docs/content/installation/_partials/provider-ordering.mdx +++ b/docs/content/installation/_partials/provider-ordering.mdx @@ -16,7 +16,6 @@ A practical starting point looks like this: ```yaml behavior: attemptTtl: 10m - joinRestrictionToggleTtl: 365d providers: - id: premium diff --git a/docs/content/installation/advanced/index.mdx b/docs/content/installation/advanced/index.mdx index f38d4b5be..0413bf5fc 100644 --- a/docs/content/installation/advanced/index.mdx +++ b/docs/content/installation/advanced/index.mdx @@ -25,7 +25,9 @@ Replication is still useful on a single runtime when restart continuity matters. If multiple runtimes will share the same replicated state, keep them on the same Identica version and give each one its own unique `serverId`. -## 1. Download the artifacts +## Installation steps + +### Download the artifacts [step] @@ -79,11 +81,11 @@ If multiple runtimes will share the same replicated state, keep them on the same -## 2. Place the runtime and provider jars +### Place the runtime and provider jars [step] -## 3. Start the runtime once and stop it again +### Start the runtime once and stop it again [step] Start the platform once so Identica can create its initial files and folders. This first boot is only meant to let the plugin prepare its default configuration layout before you begin the shared-state setup. @@ -96,17 +98,17 @@ Once that first boot has completed and the files were created, stop the proxy cl -## 4. Enable and order the providers +### Enable and order the providers [step] If hostname-based routing is not part of this rollout yet, leave `entrypoints` empty or replace the default values later when you know the real hostnames. -## 5. Understand journey mode and journey policy +### Understand journey mode and journey policy [step] -## 6. Turn shared state into an intentional environment contract +### Turn shared state into an intentional environment contract [step] Open the generated `replication` config and change it from generated defaults into a stable environment definition. @@ -131,7 +133,7 @@ redis: If a second runtime should share this replicated environment, keep the same Redis connection details, channel names, and cache namespaces, but give that runtime a different `serverId`, for example `velocity-fallback`. -## 7. Keep channels and cache namespaces stable +### Keep channels and cache namespaces stable [step] Once an environment starts using replicated state, treat these names as part of its contract: @@ -143,7 +145,7 @@ That stability is what lets sessions, reservations, attempts, and pending pipeli Do not casually rename those keys after the environment is live. If you need environment-specific naming, decide it before the rollout and then keep it stable. -## 8. Replace default routing targets with real ones +### Replace default routing targets with real ones [step] Open `routing` and align targets with the backend names that actually exist. The default values are examples only. @@ -356,7 +358,7 @@ In most cases, step targets should point to an authentication or transition dest -## 9. Review security and refine the setup +### Review security and refine the setup [step] Do not stop at the first successful startup. After the baseline works, walk through the generated configuration and the rest of the docs so you understand what the plugin will do in your network. @@ -376,7 +378,7 @@ For example, credential requirements may be stronger than what your players are Many networks handle that with a separate blocker or filter plugin. Identica does not treat that as core identity-plugin functionality. -## 10. Get help or report issues +### Get help or report issues [step] If you find a bug, need support, or want a feature, we are happy to help: diff --git a/docs/content/installation/normal/index.mdx b/docs/content/installation/normal/index.mdx index 0e3d964d9..0be989abc 100644 --- a/docs/content/installation/normal/index.mdx +++ b/docs/content/installation/normal/index.mdx @@ -22,7 +22,9 @@ Those broader provider choices are not covered by this installation page. If you do need sessions and pending state to survive plugin restarts, use [Advanced installation](../advanced/index.mdx). That requires replication to be enabled. -## 1. Download the artifacts +## Installation steps + +### Download the artifacts [step] @@ -76,11 +78,11 @@ That requires replication to be enabled. -## 2. Place the runtime and provider jars +### Place the runtime and provider jars [step] -## 3. Start the runtime once and stop it again +### Start the runtime once and stop it again [step] Start the platform once so Identica can create its initial files and folders. This first boot is only meant to let the plugin prepare its default configuration layout. @@ -97,18 +99,18 @@ Once that first boot has completed and the files were created, stop the proxy cl This page stays procedural on purpose. The detailed meaning of each config area belongs in [Configuration](../../configuration/index.md). -## 4. Enable and order the providers +### Enable and order the providers [step] If you are not using hostname-based routing yet, leave `entrypoints` empty or replace the default values later when you know the real hostnames. Keep the generated verification blocks unless verification setup is intentionally part of this installation pass. -## 5. Understand journey mode and journey policy +### Understand journey mode and journey policy [step] -## 6. Replace default routing targets with a simple real route +### Replace default routing targets with a simple real route [step] The generated `routing` defaults use example routing targets. Do not leave them unchanged unless those names really exist in your environment. @@ -166,7 +168,7 @@ In most cases, step targets should point to an authentication or transition dest -## 7. Review security and refine the setup +### Review security and refine the setup [step] Do not stop at the first successful startup. After the baseline works, walk through the generated configuration and the rest of the docs so you understand what the plugin will do in your network. @@ -186,7 +188,7 @@ For example, credential requirements may be stronger than what your players are Many networks handle that with a separate blocker or filter plugin. Identica does not treat that as core identity-plugin functionality. -## 8. Get help or report issues +### Get help or report issues [step] If you find a bug, need support, or want a feature, we are happy to help: diff --git a/docs/content/meta.json b/docs/content/meta.json index 80f55bd66..7b1f4cbf4 100644 --- a/docs/content/meta.json +++ b/docs/content/meta.json @@ -1,11 +1,16 @@ { "title": "Docs", "pages": [ + "---Getting Started---", "index", "installation", + "---Extensions---", "providers", + "addons", + "---Reference---", "configuration", "developer", - "support" + "support", + "faq" ] } diff --git a/docs/content/providers/community/index.md b/docs/content/providers/community/index.md index e537fd41c..47cb67b4c 100644 --- a/docs/content/providers/community/index.md +++ b/docs/content/providers/community/index.md @@ -1,8 +1,11 @@ --- title: Community Providers -description: Entry point for unofficial and community-maintained providers. +description: Entry point for community-maintained providers. --- +Community providers are provider modules made by the community and not maintained by Identica developers. +They still participate directly in Identica's provider flow. + This page is the routing point for unofficial and community-maintained Identica providers. ## Add a provider diff --git a/docs/content/providers/index.md b/docs/content/providers/index.md index d7cfddc23..36b21e6f8 100644 --- a/docs/content/providers/index.md +++ b/docs/content/providers/index.md @@ -6,9 +6,17 @@ description: Discover the broader provider ecosystem around Identica. Providers are the center of Identica's design. Instead of treating authentication methods as hard-coded built-ins, Identica treats them as separate provider modules that can be installed, configured, prioritized, and replaced. +Identica has two provider groups: + +- official providers maintained by Identica developers +- community providers made by the community and not maintained by Identica developers + +Community providers may follow different release timing, support expectations, documentation quality, configuration style, or feature scope than official providers. +Review them carefully before you install them. + ## Provider discovery -Use these pages to discover the broader provider ecosystem: +Use these pages to discover provider modules: - [Official providers](./official/index.md) - [Community providers](./community/index.md) diff --git a/docs/content/providers/meta.json b/docs/content/providers/meta.json index bb2878b1c..db8d2e53b 100644 --- a/docs/content/providers/meta.json +++ b/docs/content/providers/meta.json @@ -6,4 +6,3 @@ "community" ] } - diff --git a/docs/content/support/index.md b/docs/content/support/index.mdx similarity index 100% rename from docs/content/support/index.md rename to docs/content/support/index.mdx diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 071092251..c10857c90 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ toolkit = "dev-8c0271b" lombok = "1.18.42" # general -attache = "dev-5e11e1c" +attache = "dev-58f53b4" configura = "dev-8cde90a" commandant = "dev-866e0f7" keystone = "dev-90d39d2" diff --git a/identica-adapter-command/src/main/java/me/whereareiam/identica/adapter/command/DefaultCommandService.java b/identica-adapter-command/src/main/java/me/whereareiam/identica/adapter/command/DefaultCommandService.java index 48f69f3c9..994296786 100644 --- a/identica-adapter-command/src/main/java/me/whereareiam/identica/adapter/command/DefaultCommandService.java +++ b/identica-adapter-command/src/main/java/me/whereareiam/identica/adapter/command/DefaultCommandService.java @@ -16,14 +16,10 @@ import me.whereareiam.identica.adapter.command.definition.CommandDefinitionAdapter; import me.whereareiam.identica.adapter.command.executor.*; import me.whereareiam.identica.adapter.command.executor.admin.*; -import me.whereareiam.identica.adapter.command.executor.verification.VerificationConfirmCommand; -import me.whereareiam.identica.adapter.command.executor.verification.VerificationEnrollmentCommand; -import me.whereareiam.identica.adapter.command.executor.verification.VerificationSelectionCommand; import me.whereareiam.identica.adapter.command.parser.PasswordParser; import me.whereareiam.identica.adapter.command.serializer.ScopedSerializerEngine; import me.whereareiam.identica.adapter.command.suggestion.CrossPlayerSuggestions; import me.whereareiam.identica.adapter.command.suggestion.ProviderIdSuggestions; -import me.whereareiam.identica.adapter.command.suggestion.VerificationMethodSuggestions; import me.whereareiam.identica.command.CommandService; import me.whereareiam.identica.model.CommandDefinition; import me.whereareiam.identica.model.config.Commands; @@ -33,6 +29,7 @@ import org.incendo.cloud.Command; import org.incendo.cloud.CommandManager; import org.incendo.cloud.permission.Permission; +import org.incendo.cloud.suggestion.SuggestionProvider; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -46,7 +43,6 @@ public class DefaultCommandService implements CommandService { private final SerializerEngine serializer; private final Injector injector; private final CrossPlayerSuggestions crossPlayerSuggestions; - private final VerificationMethodSuggestions verificationMethodSuggestions; private final ProviderIdSuggestions providerIdSuggestions; private final Map registeredDefinitions = new HashMap<>(); @@ -60,7 +56,6 @@ public DefaultCommandService( SerializerEngine serializer, Injector injector, CrossPlayerSuggestions crossPlayerSuggestions, - VerificationMethodSuggestions verificationMethodSuggestions, ProviderIdSuggestions providerIdSuggestions ) { this.commandsProvider = commandsProvider; @@ -69,7 +64,6 @@ public DefaultCommandService( this.serializer = serializer; this.injector = injector; this.crossPlayerSuggestions = crossPlayerSuggestions; - this.verificationMethodSuggestions = verificationMethodSuggestions; this.providerIdSuggestions = providerIdSuggestions; initialize(); @@ -87,17 +81,11 @@ private void initialize() { injector.getInstance(AdminRootCommand.class), injector.getInstance(ClearCommand.class), injector.getInstance(DeleteCommand.class), - injector.getInstance(ProviderRestrictionCommand.class), injector.getInstance(ReservationCommand.class), injector.getInstance(SessionsCommand.class), injector.getInstance(EnrollCommand.class), injector.getInstance(MigrationCommand.class), - injector.getInstance(AvailabilityCommand.class), - injector.getInstance(me.whereareiam.identica.adapter.command.executor.verification.VerificationCommand.class), - injector.getInstance(VerificationEnrollmentCommand.class), - injector.getInstance(VerificationConfirmCommand.class), - injector.getInstance(VerificationSelectionCommand.class), - injector.getInstance(VerificationResetCommand.class) + injector.getInstance(AvailabilityCommand.class) ); registerExceptionHandlers(commandManager); @@ -130,6 +118,14 @@ public void registerCommandInstances(@NotNull Map def registerInternal(commandInstances); } + @Override + public void registerSuggestionProvider(@NotNull String key, @NotNull Object suggestionProvider) { + commandManagerProvider.get().parserRegistry().registerSuggestionProvider( + key, + (SuggestionProvider) suggestionProvider + ); + } + @Override public int getCommandCount() { return commandManagerProvider.get().commands().size(); @@ -160,8 +156,6 @@ private void registerInternal(Object... commandInstances) { private void registerSuggestions(@NotNull CommandManager commandManager) { commandManager.parserRegistry() .registerSuggestionProvider(CrossPlayerSuggestions.KEY, crossPlayerSuggestions); - commandManager.parserRegistry() - .registerSuggestionProvider(VerificationMethodSuggestions.KEY, verificationMethodSuggestions); commandManager.parserRegistry() .registerSuggestionProvider(ProviderIdSuggestions.KEY, providerIdSuggestions); } diff --git a/identica-adapter-command/src/main/java/me/whereareiam/identica/adapter/command/executor/EnrollCommand.java b/identica-adapter-command/src/main/java/me/whereareiam/identica/adapter/command/executor/EnrollCommand.java index 137fbeef1..9e7eba64c 100644 --- a/identica-adapter-command/src/main/java/me/whereareiam/identica/adapter/command/executor/EnrollCommand.java +++ b/identica-adapter-command/src/main/java/me/whereareiam/identica/adapter/command/executor/EnrollCommand.java @@ -14,10 +14,10 @@ import me.whereareiam.identica.model.config.Engine; import me.whereareiam.identica.model.config.Messages; import me.whereareiam.identica.model.pipeline.journey.JourneyStateItem; -import me.whereareiam.identica.model.pipeline.state.PipelineState; -import me.whereareiam.identica.model.pipeline.state.PipelineStateReference; import me.whereareiam.identica.model.provider.ProviderContext; import me.whereareiam.identica.pipeline.ScenarioContext; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.PipelineStateReference; import me.whereareiam.identica.pipeline.state.PipelineStateStore; import me.whereareiam.identica.type.pipeline.PipelineType; import me.whereareiam.identica.type.pipeline.journey.StageType; diff --git a/identica-adapter-command/src/main/java/me/whereareiam/identica/adapter/command/executor/MigrationCommand.java b/identica-adapter-command/src/main/java/me/whereareiam/identica/adapter/command/executor/MigrationCommand.java index 0dee18941..69efbd104 100644 --- a/identica-adapter-command/src/main/java/me/whereareiam/identica/adapter/command/executor/MigrationCommand.java +++ b/identica-adapter-command/src/main/java/me/whereareiam/identica/adapter/command/executor/MigrationCommand.java @@ -27,7 +27,6 @@ import me.whereareiam.identica.type.migration.MigrationCancelScope; import me.whereareiam.identica.type.migration.MigrationInitiator; import me.whereareiam.identica.type.migration.MigrationResultStatus; -import me.whereareiam.identica.type.provider.ProviderCapability; import me.whereareiam.identica.util.UniqueIdUtil; import me.whereareiam.keystone.Actor; import me.whereareiam.keystone.model.SerializerContent; @@ -84,13 +83,7 @@ public void start( ) { Messages.Commands.Migration messages = messagesProvider.get().getCommands().getMigration(); ResolvedTarget resolved = resolveTarget(sender, target, messages); - if (resolved == null) - return; - - if (!supportsMigration(providerId)) { - sendMessage(sender, messages.getStart().getProviderUnsupported(), Map.of("provider", providerId)); - return; - } + if (resolved == null) return; MigrationResult result = migrationService.start(MigrationStart.builder() .connectionUniqueId(resolved.uniqueId()) @@ -129,6 +122,14 @@ public void start( return; } } + if (result.getStatus() == MigrationResultStatus.TARGET_UNSUPPORTED) { + sendMessage(sender, messages.getStart().getProviderUnsupported(), Map.of("provider", providerId)); + return; + } + if (result.getStatus() == MigrationResultStatus.PROVIDER_UNAVAILABLE) { + sendMessage(sender, messages.getStart().getProviderUnavailable(), Map.of("provider", providerId)); + return; + } if (result.getStatus() == MigrationResultStatus.STARTED) { sendMessage(sender, messages.getStart().getStarted(), Map.of( @@ -280,17 +281,6 @@ private ResolvedTarget resolveTarget( return new ResolvedTarget(account.getUniqueId(), account.getUsername()); } - private boolean supportsMigration(@NotNull String providerId) { - for (InternalProvider provider : providerManager.findProviders(ProviderCapability.MIGRATION)) { - if (provider == null || provider.getDescriptor() == null) - continue; - String id = provider.getDescriptor().getId(); - if (id.equalsIgnoreCase(providerId)) - return true; - } - return false; - } - private Map resolveProviderNames() { Map names = new HashMap<>(); for (InternalProvider provider : providerManager.getProviders()) { diff --git a/identica-adapter-command/src/test/java/me/whereareiam/identica/adapter/command/executor/MigrationCommandTest.java b/identica-adapter-command/src/test/java/me/whereareiam/identica/adapter/command/executor/MigrationCommandTest.java new file mode 100644 index 000000000..ecee33e7f --- /dev/null +++ b/identica-adapter-command/src/test/java/me/whereareiam/identica/adapter/command/executor/MigrationCommandTest.java @@ -0,0 +1,183 @@ +package me.whereareiam.identica.adapter.command.executor; + +import me.whereareiam.identica.Serializer; +import me.whereareiam.identica.database.AccountPersistenceService; +import me.whereareiam.identica.database.provider.ProviderLinkPersistenceService; +import me.whereareiam.identica.database.provider.ProviderProfilePersistenceService; +import me.whereareiam.identica.identity.IdentityService; +import me.whereareiam.identica.model.config.Messages; +import me.whereareiam.identica.model.identity.Account; +import me.whereareiam.identica.model.migration.operation.MigrationResult; +import me.whereareiam.identica.provider.ProviderManager; +import me.whereareiam.identica.service.MigrationService; +import me.whereareiam.identica.type.migration.MigrationResultStatus; +import me.whereareiam.keystone.Actor; +import me.whereareiam.keystone.model.SerializerContent; +import me.whereareiam.keystone.model.SerializerOptions; +import me.whereareiam.keystone.serializer.SerializerEngine; +import net.kyori.adventure.audience.Audience; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Locale; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@DisplayName("Migration Command") +class MigrationCommandTest { + @BeforeAll + static void initializeSerializer() { + Serializer.initialize(() -> TEST_SERIALIZER); + } + + private static final SerializerEngine TEST_SERIALIZER = new SerializerEngine() { + @Override + public @NotNull String serialize(@NotNull Component component) { + return component.toString(); + } + + @Override + public @NotNull Component serialize(@NotNull SerializerContent content) { + return Component.text(content.getMessage()); + } + + @Override + public @NotNull SerializerOptions.PlaceholderFormat getPlaceholderFormat() { + return SerializerOptions.PlaceholderFormat.CURLY_BRACES; + } + }; + + @Mock + private AccountPersistenceService accountPersistenceService; + @Mock + private ProviderLinkPersistenceService providerLinkPersistenceService; + @Mock + private ProviderProfilePersistenceService providerProfilePersistenceService; + @Mock + private ProviderManager providerManager; + @Mock + private MigrationService migrationService; + @Mock + private IdentityService identityService; + + @Test + @DisplayName("Shows provider unsupported when the service rejects the target") + void startShowsProviderUnsupported() { + TestActor sender = new TestActor(); + Account target = account(); + when(accountPersistenceService.findByUniqueId(target.getUniqueId())).thenReturn(Optional.of(target)); + when(migrationService.start(any())).thenReturn(MigrationResult.builder() + .status(MigrationResultStatus.TARGET_UNSUPPORTED) + .build()); + + MigrationCommand command = new MigrationCommand( + this::messages, + accountPersistenceService, + providerLinkPersistenceService, + providerProfilePersistenceService, + providerManager, + migrationService, + identityService + ); + + command.start(sender, target.getUniqueId().toString(), "premium"); + + assertEquals("unsupported", PlainTextComponentSerializer.plainText().serialize(sender.lastMessage())); + } + + @Test + @DisplayName("Shows provider unavailable when the service rejects an inactive target") + void startShowsProviderUnavailable() { + TestActor sender = new TestActor(); + Account target = account(); + when(accountPersistenceService.findByUniqueId(target.getUniqueId())).thenReturn(Optional.of(target)); + when(migrationService.start(any())).thenReturn(MigrationResult.builder() + .status(MigrationResultStatus.PROVIDER_UNAVAILABLE) + .build()); + + MigrationCommand command = new MigrationCommand( + this::messages, + accountPersistenceService, + providerLinkPersistenceService, + providerProfilePersistenceService, + providerManager, + migrationService, + identityService + ); + + command.start(sender, target.getUniqueId().toString(), "premium"); + + assertEquals("unavailable", PlainTextComponentSerializer.plainText().serialize(sender.lastMessage())); + } + + private Account account() { + return Account.builder() + .uniqueId(UUID.randomUUID()) + .username("PlayerOne") + .build(); + } + + private Messages messages() { + Messages messages = new Messages(); + Messages.Commands commands = new Messages.Commands(); + Messages.Commands.Migration migration = new Messages.Commands.Migration(); + Messages.Commands.Migration.Start start = new Messages.Commands.Migration.Start(); + start.setProviderUnsupported("unsupported"); + start.setProviderUnavailable("unavailable"); + migration.setStart(start); + commands.setMigration(migration); + messages.setCommands(commands); + return messages; + } + + private static final class TestActor implements Actor { + private final AtomicReference lastMessage = new AtomicReference<>(); + + @Override + public UUID getUniqueId() { + return UUID.randomUUID(); + } + + @Override + public String getUsername() { + return "Admin"; + } + + @Override + public void sendMessage(Component message) { + lastMessage.set(message); + } + + @Override + public boolean hasPermission(String permission) { + return true; + } + + @Override + public Locale getLocale() { + return Locale.ENGLISH; + } + + @Override + public Audience getAudience() { + return Audience.empty(); + } + + private Component lastMessage() { + return lastMessage.get(); + } + } +} diff --git a/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/DatabaseConfiguration.java b/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/DatabaseConfiguration.java index 087f58519..4fcce46a2 100644 --- a/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/DatabaseConfiguration.java +++ b/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/DatabaseConfiguration.java @@ -12,13 +12,9 @@ import me.whereareiam.identica.adapter.database.repository.account.AccountReservationRepository; import me.whereareiam.identica.adapter.database.repository.provider.ProviderLinkRepository; import me.whereareiam.identica.adapter.database.repository.provider.ProviderProfileRepository; -import me.whereareiam.identica.adapter.database.repository.username.UsernameHistoryRepository; -import me.whereareiam.identica.adapter.database.repository.verification.VerificationEnrollmentRepository; -import me.whereareiam.identica.adapter.database.repository.verification.VerificationRecoveryCodeRepository; -import me.whereareiam.identica.adapter.database.repository.verification.VerificationSelectionRepository; -import me.whereareiam.identica.adapter.database.username.DefaultUsernameHistoryPersistenceService; -import me.whereareiam.identica.adapter.database.verification.DefaultVerificationPersistenceService; -import me.whereareiam.identica.database.*; +import me.whereareiam.identica.database.AccountPersistenceService; +import me.whereareiam.identica.database.AccountReservationPersistenceService; +import me.whereareiam.identica.database.DatabaseService; import me.whereareiam.identica.database.provider.ProviderLinkPersistenceService; import me.whereareiam.identica.database.provider.ProviderProfilePersistenceService; import me.whereareiam.identica.database.schema.SchemaBootstrap; @@ -34,8 +30,6 @@ protected void configure() { bind(AccountReservationPersistenceService.class).to(DefaultAccountReservationPersistenceService.class).asEagerSingleton(); bind(ProviderLinkPersistenceService.class).to(DefaultProviderLinkPersistenceService.class).asEagerSingleton(); bind(ProviderProfilePersistenceService.class).to(DefaultProviderProfilePersistenceService.class).asEagerSingleton(); - bind(UsernameHistoryPersistenceService.class).to(DefaultUsernameHistoryPersistenceService.class).asEagerSingleton(); - bind(VerificationPersistenceService.class).to(DefaultVerificationPersistenceService.class).asEagerSingleton(); } @Provides @@ -62,27 +56,4 @@ public ProviderProfileRepository provideProviderProfileRepository(Jdbi jdbi) { return jdbi.onDemand(ProviderProfileRepository.class); } - @Provides - @Singleton - public UsernameHistoryRepository provideUsernameHistoryRepository(Jdbi jdbi) { - return jdbi.onDemand(UsernameHistoryRepository.class); - } - - @Provides - @Singleton - public VerificationEnrollmentRepository provideVerificationEnrollmentRepository(Jdbi jdbi) { - return jdbi.onDemand(VerificationEnrollmentRepository.class); - } - - @Provides - @Singleton - public VerificationSelectionRepository provideVerificationSelectionRepository(Jdbi jdbi) { - return jdbi.onDemand(VerificationSelectionRepository.class); - } - - @Provides - @Singleton - public VerificationRecoveryCodeRepository provideVerificationRecoveryCodeRepository(Jdbi jdbi) { - return jdbi.onDemand(VerificationRecoveryCodeRepository.class); - } } diff --git a/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/account/DefaultAccountPersistenceService.java b/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/account/DefaultAccountPersistenceService.java index 3ce99d5a9..236b286be 100644 --- a/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/account/DefaultAccountPersistenceService.java +++ b/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/account/DefaultAccountPersistenceService.java @@ -13,7 +13,6 @@ import me.whereareiam.identica.event.base.IdenticEvent; import me.whereareiam.identica.logging.Logger; import me.whereareiam.identica.model.identity.Account; -import me.whereareiam.identica.type.UsernameSource; import me.whereareiam.identica.type.event.EventOrder; import org.jetbrains.annotations.NotNull; @@ -74,7 +73,6 @@ public long count() { accountRepository.insert( entity.getUniqueId(), entity.getUsername(), - entity.getUsernameSource(), entity.getCreatedAt(), entity.getLastSeenAt() ); @@ -106,15 +104,6 @@ public void updateUsername(@NotNull UUID uniqueId, @NotNull String username) { } } - @Override - public void updateUsernameSource(@NotNull UUID uniqueId, @NotNull UsernameSource source) { - try { - accountRepository.updateUsernameSource(uniqueId, source.getId()); - } catch (Exception e) { - Logger.warn("Failed to update username source for %s: %s", uniqueId, e.getMessage()); - } - } - @Override public void delete(@NotNull UUID uniqueId) { try { diff --git a/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/entity/account/AccountEntity.java b/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/entity/account/AccountEntity.java index 074fb1fad..cf39b466a 100644 --- a/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/entity/account/AccountEntity.java +++ b/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/entity/account/AccountEntity.java @@ -1,10 +1,6 @@ package me.whereareiam.identica.adapter.database.entity.account; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.*; import me.whereareiam.dialectica.EntitySchemaProvider; import me.whereareiam.dialectica.annotation.Entity; @@ -19,7 +15,6 @@ public class AccountEntity implements EntitySchemaProvider { private UUID uniqueId; private String username; - private String usernameSource; private long createdAt; private long lastSeenAt; @@ -40,7 +35,6 @@ public String statement(String databaseType) { CREATE TABLE IF NOT EXISTS identica_accounts ( unique_id %s PRIMARY KEY, username VARCHAR(64) NOT NULL, - username_source VARCHAR(32) NOT NULL, created_at %s, last_seen_at %s ) diff --git a/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/mapper/UsernameHistoryMapper.java b/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/mapper/UsernameHistoryMapper.java deleted file mode 100644 index 0ad8260c3..000000000 --- a/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/mapper/UsernameHistoryMapper.java +++ /dev/null @@ -1,30 +0,0 @@ -package me.whereareiam.identica.adapter.database.mapper; - -import me.whereareiam.identica.adapter.database.entity.UsernameHistoryEntity; -import me.whereareiam.identica.model.UsernameHistoryEntry; - -public final class UsernameHistoryMapper { - public static UsernameHistoryEntry toModel(UsernameHistoryEntity entity) { - if (entity == null) return null; - return UsernameHistoryEntry.builder() - .uniqueId(entity.getUniqueId()) - .providerId(entity.getProviderId()) - .oldUsername(entity.getOldUsername()) - .newUsername(entity.getNewUsername()) - .source(entity.getSource()) - .changedAt(entity.getChangedAt()) - .build(); - } - - public static UsernameHistoryEntity toEntity(UsernameHistoryEntry entry) { - if (entry == null) return null; - return UsernameHistoryEntity.builder() - .uniqueId(entry.getUniqueId()) - .providerId(entry.getProviderId()) - .oldUsername(entry.getOldUsername()) - .newUsername(entry.getNewUsername()) - .source(entry.getSource()) - .changedAt(entry.getChangedAt()) - .build(); - } -} diff --git a/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/mapper/account/AccountMapper.java b/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/mapper/account/AccountMapper.java index 8807abc13..0231a004c 100644 --- a/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/mapper/account/AccountMapper.java +++ b/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/mapper/account/AccountMapper.java @@ -2,7 +2,6 @@ import me.whereareiam.identica.adapter.database.entity.account.AccountEntity; import me.whereareiam.identica.model.identity.Account; -import me.whereareiam.identica.type.UsernameSource; public final class AccountMapper { public static Account toModel(AccountEntity entity) { @@ -10,7 +9,6 @@ public static Account toModel(AccountEntity entity) { return Account.builder() .uniqueId(entity.getUniqueId()) .username(entity.getUsername()) - .source(UsernameSource.fromId(entity.getUsernameSource())) .createdAt(entity.getCreatedAt()) .lastSeenAt(entity.getLastSeenAt()) .build(); @@ -21,7 +19,6 @@ public static AccountEntity toEntity(Account account) { return AccountEntity.builder() .uniqueId(account.getUniqueId()) .username(account.getUsername()) - .usernameSource(account.getSource().getId()) .createdAt(account.getCreatedAt()) .lastSeenAt(account.getLastSeenAt()) .build(); diff --git a/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/repository/account/AccountRepository.java b/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/repository/account/AccountRepository.java index 4fb732402..1fe806baa 100644 --- a/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/repository/account/AccountRepository.java +++ b/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/repository/account/AccountRepository.java @@ -15,7 +15,6 @@ public interface AccountRepository { @SqlQuery(""" SELECT unique_id AS uniqueId, username AS username, - username_source AS usernameSource, created_at AS createdAt, last_seen_at AS lastSeenAt FROM identica_accounts @@ -26,7 +25,6 @@ public interface AccountRepository { @SqlQuery(""" SELECT unique_id AS uniqueId, username AS username, - username_source AS usernameSource, created_at AS createdAt, last_seen_at AS lastSeenAt FROM identica_accounts @@ -38,13 +36,12 @@ WHERE LOWER(username) = LOWER(:username) long count(); @SqlUpdate(""" - INSERT INTO identica_accounts (unique_id, username, username_source, created_at, last_seen_at) - VALUES (:uniqueId, :username, :usernameSource, :createdAt, :lastSeenAt) + INSERT INTO identica_accounts (unique_id, username, created_at, last_seen_at) + VALUES (:uniqueId, :username, :createdAt, :lastSeenAt) """) void insert( @Bind("uniqueId") UUID uniqueId, @Bind("username") String username, - @Bind("usernameSource") String usernameSource, @Bind("createdAt") long createdAt, @Bind("lastSeenAt") long lastSeenAt ); @@ -69,16 +66,6 @@ void updateUsername( @Bind("username") String username ); - @SqlUpdate(""" - UPDATE identica_accounts - SET username_source = :usernameSource - WHERE unique_id = :uniqueId - """) - void updateUsernameSource( - @Bind("uniqueId") UUID uniqueId, - @Bind("usernameSource") String usernameSource - ); - @SqlUpdate("DELETE FROM identica_accounts WHERE unique_id = :uniqueId") void delete(@Bind("uniqueId") UUID uniqueId); } diff --git a/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/username/DefaultUsernameHistoryPersistenceService.java b/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/username/DefaultUsernameHistoryPersistenceService.java deleted file mode 100644 index f95be1e3e..000000000 --- a/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/username/DefaultUsernameHistoryPersistenceService.java +++ /dev/null @@ -1,64 +0,0 @@ -package me.whereareiam.identica.adapter.database.username; - -import com.google.inject.Inject; -import com.google.inject.Singleton; -import me.whereareiam.identica.adapter.database.entity.UsernameHistoryEntity; -import me.whereareiam.identica.adapter.database.mapper.UsernameHistoryMapper; -import me.whereareiam.identica.adapter.database.repository.username.UsernameHistoryRepository; -import me.whereareiam.identica.database.UsernameHistoryPersistenceService; -import me.whereareiam.identica.event.EventListener; -import me.whereareiam.identica.event.EventManager; -import me.whereareiam.identica.event.account.AccountLifecycleEvent; -import me.whereareiam.identica.event.base.IdenticEvent; -import me.whereareiam.identica.model.UsernameHistoryEntry; -import me.whereareiam.identica.type.event.EventOrder; -import org.jetbrains.annotations.NotNull; - -import java.util.UUID; - -@Singleton -public class DefaultUsernameHistoryPersistenceService implements UsernameHistoryPersistenceService, EventListener { - private final UsernameHistoryRepository repository; - - @Inject - public DefaultUsernameHistoryPersistenceService( - @NotNull UsernameHistoryRepository repository, - @NotNull EventManager eventManager - ) { - this.repository = repository; - eventManager.register(this); - } - - @Override - public void record(@NotNull UsernameHistoryEntry entry) { - if (entry.getOldUsername().isBlank()) - throw new IllegalArgumentException("Username history old username is required"); - if (entry.getNewUsername().isBlank()) - throw new IllegalArgumentException("Username history new username is required"); - if (entry.getSource().isBlank()) - throw new IllegalArgumentException("Username history source is required"); - - UsernameHistoryEntity entity = UsernameHistoryMapper.toEntity(entry); - repository.insert( - entity.getUniqueId(), - entity.getProviderId(), - entity.getOldUsername(), - entity.getNewUsername(), - entity.getSource(), - entity.getChangedAt() - ); - } - - @Override - public void deleteAll(@NotNull UUID uniqueId) { - repository.deleteAll(uniqueId); - } - - @IdenticEvent(EventOrder.HIGH) - public void onAccountLifecycle(@NotNull AccountLifecycleEvent event) { - UUID uniqueId = event.getIdentity().getAccountUniqueId(); - if (uniqueId == null) return; - - deleteAll(uniqueId); - } -} diff --git a/identica-adapter-database/src/test/java/me/whereareiam/identica/adapter/database/account/DefaultAccountPersistenceServiceTest.java b/identica-adapter-database/src/test/java/me/whereareiam/identica/adapter/database/account/DefaultAccountPersistenceServiceTest.java index f8a16093e..907ab3cf4 100644 --- a/identica-adapter-database/src/test/java/me/whereareiam/identica/adapter/database/account/DefaultAccountPersistenceServiceTest.java +++ b/identica-adapter-database/src/test/java/me/whereareiam/identica/adapter/database/account/DefaultAccountPersistenceServiceTest.java @@ -9,7 +9,6 @@ import me.whereareiam.identica.event.account.AccountLifecycleEvent; import me.whereareiam.identica.identity.actor.ConnectionIdentity; import me.whereareiam.identica.model.identity.Account; -import me.whereareiam.identica.type.UsernameSource; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -21,14 +20,8 @@ import java.util.Optional; import java.util.UUID; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.when; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) @DisplayName("Default Account Persistence Service") @@ -52,7 +45,6 @@ void findByUniqueIdMapsEntityToModel() { AccountEntity entity = AccountEntity.builder() .uniqueId(uniqueId) .username("PlayerOne") - .usernameSource("manual") .createdAt(TestDataFactory.CREATED_AT) .lastSeenAt(TestDataFactory.LAST_SEEN_AT) .build(); @@ -63,7 +55,6 @@ void findByUniqueIdMapsEntityToModel() { assertTrue(result.isPresent()); assertEquals(uniqueId, result.get().getUniqueId()); assertEquals("PlayerOne", result.get().getUsername()); - assertEquals(UsernameSource.MANUAL, result.get().getSource()); assertEquals(TestDataFactory.CREATED_AT, result.get().getCreatedAt()); assertEquals(TestDataFactory.LAST_SEEN_AT, result.get().getLastSeenAt()); } @@ -107,7 +98,6 @@ void createDelegatesToRepository() { verify(accountRepository).insert( uniqueId, "PlayerOne", - UsernameSource.MANUAL.getId(), TestDataFactory.CREATED_AT, TestDataFactory.LAST_SEEN_AT ); @@ -141,16 +131,6 @@ void updateUsernameCallsRepository() { verify(accountRepository).updateUsername(uniqueId, "PlayerTwo"); } - @DisplayName("Stores the username source as its configured identifier") - @Test - void updateUsernameSourceDelegatesId() { - UUID uniqueId = UUID.randomUUID(); - - service.updateUsernameSource(uniqueId, UsernameSource.SYSTEM); - - verify(accountRepository).updateUsernameSource(uniqueId, UsernameSource.SYSTEM.getId()); - } - @DisplayName("Swallows repository errors when deleting an account") @Test void deleteSwallowsExceptions() { diff --git a/identica-adapter-database/src/test/java/me/whereareiam/identica/adapter/database/integration/AccountSqlBehaviorIntegrationTest.java b/identica-adapter-database/src/test/java/me/whereareiam/identica/adapter/database/integration/AccountSqlBehaviorIntegrationTest.java index 40e96875e..b89a105d1 100644 --- a/identica-adapter-database/src/test/java/me/whereareiam/identica/adapter/database/integration/AccountSqlBehaviorIntegrationTest.java +++ b/identica-adapter-database/src/test/java/me/whereareiam/identica/adapter/database/integration/AccountSqlBehaviorIntegrationTest.java @@ -22,10 +22,9 @@ void findByUsernameIsCaseInsensitive(DatabaseFixture fixture) { List results = fixture.withHandle(handle -> { handle.execute( - "INSERT INTO identica_accounts (unique_id, username, username_source, created_at, last_seen_at) VALUES (?, ?, ?, ?, ?)", + "INSERT INTO identica_accounts (unique_id, username, created_at, last_seen_at) VALUES (?, ?, ?, ?)", uniqueId, username, - "manual", 1L, 2L ); diff --git a/identica-adapter-database/src/test/java/me/whereareiam/identica/adapter/database/integration/SchemaConstraintsIntegrationTest.java b/identica-adapter-database/src/test/java/me/whereareiam/identica/adapter/database/integration/SchemaConstraintsIntegrationTest.java index 8e978407b..7ac4b44ef 100644 --- a/identica-adapter-database/src/test/java/me/whereareiam/identica/adapter/database/integration/SchemaConstraintsIntegrationTest.java +++ b/identica-adapter-database/src/test/java/me/whereareiam/identica/adapter/database/integration/SchemaConstraintsIntegrationTest.java @@ -21,10 +21,9 @@ void cascadesDeletesFromAccounts(DatabaseFixture fixture) { fixture.useHandle(handle -> { handle.execute( - "INSERT INTO identica_accounts (unique_id, username, username_source, created_at, last_seen_at) VALUES (?, ?, ?, ?, ?)", + "INSERT INTO identica_accounts (unique_id, username, created_at, last_seen_at) VALUES (?, ?, ?, ?)", uniqueId, "Player", - "manual", 1L, 2L ); @@ -67,10 +66,9 @@ void enforcesUniqueProviderLinkConstraint(DatabaseFixture fixture) { fixture.useHandle(handle -> { handle.execute( - "INSERT INTO identica_accounts (unique_id, username, username_source, created_at, last_seen_at) VALUES (?, ?, ?, ?, ?)", + "INSERT INTO identica_accounts (unique_id, username, created_at, last_seen_at) VALUES (?, ?, ?, ?)", uniqueId, "Player", - "manual", 1L, 2L ); diff --git a/identica-adapter-database/src/test/java/me/whereareiam/identica/adapter/database/testing/AbstractDatabaseFixture.java b/identica-adapter-database/src/test/java/me/whereareiam/identica/adapter/database/testing/AbstractDatabaseFixture.java index 3c37185b8..29b1d0d0b 100644 --- a/identica-adapter-database/src/test/java/me/whereareiam/identica/adapter/database/testing/AbstractDatabaseFixture.java +++ b/identica-adapter-database/src/test/java/me/whereareiam/identica/adapter/database/testing/AbstractDatabaseFixture.java @@ -50,7 +50,6 @@ public void reset() { handle.execute("DELETE FROM identica_provider_profiles"); handle.execute("DELETE FROM identica_provider_links"); handle.execute("DELETE FROM identica_accounts"); - handle.execute("DELETE FROM identica_username_history"); }); } diff --git a/identica-adapter-database/src/test/java/me/whereareiam/identica/adapter/database/testing/TestDataFactory.java b/identica-adapter-database/src/test/java/me/whereareiam/identica/adapter/database/testing/TestDataFactory.java index acf2e05db..c60b73b9d 100644 --- a/identica-adapter-database/src/test/java/me/whereareiam/identica/adapter/database/testing/TestDataFactory.java +++ b/identica-adapter-database/src/test/java/me/whereareiam/identica/adapter/database/testing/TestDataFactory.java @@ -1,10 +1,8 @@ package me.whereareiam.identica.adapter.database.testing; -import me.whereareiam.identica.model.UsernameHistoryEntry; import me.whereareiam.identica.model.identity.Account; import me.whereareiam.identica.model.identity.provider.AccountProviderLink; import me.whereareiam.identica.model.identity.provider.AccountProviderProfile; -import me.whereareiam.identica.type.UsernameSource; import java.util.UUID; @@ -12,16 +10,11 @@ public final class TestDataFactory { public static final long CREATED_AT = 1_700_000_000_000L; public static final long LAST_SEEN_AT = 1_700_000_000_500L; public static final long LINKED_AT = 1_700_000_001_000L; - public static final long CHANGED_AT = 1_700_000_002_000L; - - private TestDataFactory() { - } public static Account account(UUID uniqueId, String username) { return Account.builder() .uniqueId(uniqueId) .username(username) - .source(UsernameSource.MANUAL) .createdAt(CREATED_AT) .lastSeenAt(LAST_SEEN_AT) .build(); @@ -45,15 +38,4 @@ public static AccountProviderProfile providerProfile(String providerId, String s .providerUsername(username) .build(); } - - public static UsernameHistoryEntry usernameHistoryEntry(UUID uniqueId, String providerId, String oldUsername, String newUsername, String source) { - return UsernameHistoryEntry.builder() - .uniqueId(uniqueId) - .providerId(providerId) - .oldUsername(oldUsername) - .newUsername(newUsername) - .source(source) - .changedAt(CHANGED_AT) - .build(); - } } diff --git a/identica-adapter-database/src/test/java/me/whereareiam/identica/adapter/database/username/DefaultUsernameHistoryPersistenceServiceTest.java b/identica-adapter-database/src/test/java/me/whereareiam/identica/adapter/database/username/DefaultUsernameHistoryPersistenceServiceTest.java deleted file mode 100644 index 14df8e544..000000000 --- a/identica-adapter-database/src/test/java/me/whereareiam/identica/adapter/database/username/DefaultUsernameHistoryPersistenceServiceTest.java +++ /dev/null @@ -1,78 +0,0 @@ -package me.whereareiam.identica.adapter.database.username; - -import me.whereareiam.identica.adapter.database.repository.username.UsernameHistoryRepository; -import me.whereareiam.identica.adapter.database.testing.TestDataFactory; -import me.whereareiam.identica.event.EventManager; -import me.whereareiam.identica.model.UsernameHistoryEntry; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.UUID; - -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.verify; - -@ExtendWith(MockitoExtension.class) -@DisplayName("Default Username-History Persistence Service") -class DefaultUsernameHistoryPersistenceServiceTest { - @Mock - private UsernameHistoryRepository repository; - @Mock - private EventManager eventManager; - - private DefaultUsernameHistoryPersistenceService service; - - @BeforeEach - void setUp() { - service = new DefaultUsernameHistoryPersistenceService(repository, eventManager); - } - - @DisplayName("Rejects history entries with a blank previous username") - @Test - void recordRejectsBlankOldUsername() { - UsernameHistoryEntry entry = TestDataFactory.usernameHistoryEntry(UUID.randomUUID(), "provider", " ", "new", "source"); - - assertThrows(IllegalArgumentException.class, () -> service.record(entry)); - } - - @DisplayName("Rejects history entries with a blank new username") - @Test - void recordRejectsBlankNewUsername() { - UsernameHistoryEntry entry = TestDataFactory.usernameHistoryEntry(UUID.randomUUID(), "provider", "old", " ", "source"); - - assertThrows(IllegalArgumentException.class, () -> service.record(entry)); - } - - @DisplayName("Rejects history entries with a blank source") - @Test - void recordRejectsBlankSource() { - UsernameHistoryEntry entry = TestDataFactory.usernameHistoryEntry(UUID.randomUUID(), "provider", "old", "new", " "); - - assertThrows(IllegalArgumentException.class, () -> service.record(entry)); - } - - @DisplayName("Persists username history entries through the repository") - @Test - void recordInsertsEntry() { - UUID uniqueId = UUID.randomUUID(); - UsernameHistoryEntry entry = TestDataFactory.usernameHistoryEntry(uniqueId, "provider", "old", "new", "source"); - - service.record(entry); - - verify(repository).insert(uniqueId, "provider", "old", "new", "source", TestDataFactory.CHANGED_AT); - } - - @DisplayName("Deletes all username history entries for an account") - @Test - void deleteAllDelegatesToRepository() { - UUID uniqueId = UUID.randomUUID(); - - service.deleteAll(uniqueId); - - verify(repository).deleteAll(uniqueId); - } -} diff --git a/identica-api/src/main/java/me/whereareiam/identica/IdenticaAPI.java b/identica-api/src/main/java/me/whereareiam/identica/IdenticaAPI.java index 09e931618..5d687c2bf 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/IdenticaAPI.java +++ b/identica-api/src/main/java/me/whereareiam/identica/IdenticaAPI.java @@ -4,21 +4,19 @@ import com.google.inject.Key; import lombok.Getter; import me.whereareiam.configura.Configura; -import me.whereareiam.identica.identity.account.RegistrationAccountService; -import me.whereareiam.identica.identity.account.AccountService; -import me.whereareiam.identica.pipeline.extension.PipelineExtensionRegistry; import me.whereareiam.identica.command.CommandService; import me.whereareiam.identica.database.DatabaseService; import me.whereareiam.identica.event.EventManager; import me.whereareiam.identica.event.lifecycle.IdenticaReadyEvent; import me.whereareiam.identica.identity.IdentityService; -import me.whereareiam.identica.service.MigrationService; +import me.whereareiam.identica.identity.account.AccountService; +import me.whereareiam.identica.identity.account.RegistrationAccountService; +import me.whereareiam.identica.identity.session.SessionService; +import me.whereareiam.identica.pipeline.extension.PipelineExtensionRegistry; import me.whereareiam.identica.provider.ProviderManager; import me.whereareiam.identica.provider.ProviderOperations; -import me.whereareiam.identica.identity.session.SessionService; import me.whereareiam.identica.replication.ReplicationSystem; -import me.whereareiam.identica.verification.VerificationService; -import me.whereareiam.identica.verification.VerificationRegistry; +import me.whereareiam.identica.service.MigrationService; import org.jetbrains.annotations.NotNull; /** @@ -267,28 +265,4 @@ public static Configura getConfigura() { return getService(Configura.class); } - /** - * Gets the VerificationService for verification enrollment, selection, - * reset, and challenge operations. - * - * @return the VerificationService instance - * @throws IllegalStateException if the API is not initialized - */ - @NotNull - public static VerificationService getVerificationService() { - return getService(VerificationService.class); - } - - /** - * Gets the VerificationRegistry for registering and resolving - * verification methods. - * - * @return the VerificationRegistry instance - * @throws IllegalStateException if the API is not initialized - */ - @NotNull - public static VerificationRegistry getVerificationRegistry() { - return getService(VerificationRegistry.class); - } - } diff --git a/identica-api/src/main/java/me/whereareiam/identica/command/CommandService.java b/identica-api/src/main/java/me/whereareiam/identica/command/CommandService.java index 8f2379614..9f294dbb6 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/command/CommandService.java +++ b/identica-api/src/main/java/me/whereareiam/identica/command/CommandService.java @@ -15,6 +15,8 @@ public interface CommandService { void registerCommandInstances(@NotNull Map definitions, @NotNull Object... commandInstances); + void registerSuggestionProvider(@NotNull String key, @NotNull Object suggestionProvider); + int getCommandCount(); @NotNull diff --git a/identica-api/src/main/java/me/whereareiam/identica/conflict/ConflictService.java b/identica-api/src/main/java/me/whereareiam/identica/conflict/ConflictService.java index 8ea9074d3..f4706544e 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/conflict/ConflictService.java +++ b/identica-api/src/main/java/me/whereareiam/identica/conflict/ConflictService.java @@ -40,14 +40,14 @@ public interface ConflictService { * * @param type conflict type */ - void register(@NotNull ConflictType type); + void register(@NotNull ConflictType type); /** * Unregister a conflict type. * * @param type conflict type */ - void unregister(@NotNull ConflictType type); + void unregister(@NotNull ConflictType type); /** * Resolve a registered conflict type by key. @@ -56,7 +56,7 @@ public interface ConflictService { * @return conflict type or {@code null} */ @Nullable - ConflictType getType(@NotNull String key); + ConflictType getType(@NotNull String key); /** * Return registered conflict types. @@ -64,14 +64,14 @@ public interface ConflictService { * @return conflict types */ @NotNull - Set getTypes(); + Set> getTypes(); /** * Resolve a conflict for the provided context. * *
{@code
 	 * ConflictResolution resolution = conflictService.resolve(context);
-	 * if (resolution != null && resolution.getAction() == ConflictResolution.Action.DENY) {
+	 * if (resolution != null && resolution.getDecision() == ConflictResolution.Decision.DENY) {
 	 *     // handle denial
 	 * }
 	 * }
diff --git a/identica-api/src/main/java/me/whereareiam/identica/conflict/ConflictSubject.java b/identica-api/src/main/java/me/whereareiam/identica/conflict/ConflictSubject.java new file mode 100644 index 000000000..7aa61ee98 --- /dev/null +++ b/identica-api/src/main/java/me/whereareiam/identica/conflict/ConflictSubject.java @@ -0,0 +1,15 @@ +package me.whereareiam.identica.conflict; + +import org.jetbrains.annotations.NotNull; + +/** + * Generic subject presented to a conflict type. + */ +public interface ConflictSubject { + /** + * Returns the hook where the conflict is being evaluated. + * + * @return hook id + */ + @NotNull String hook(); +} diff --git a/identica-api/src/main/java/me/whereareiam/identica/conflict/ConflictType.java b/identica-api/src/main/java/me/whereareiam/identica/conflict/ConflictType.java index 6a6438121..8600b5e55 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/conflict/ConflictType.java +++ b/identica-api/src/main/java/me/whereareiam/identica/conflict/ConflictType.java @@ -1,10 +1,9 @@ package me.whereareiam.identica.conflict; +import com.fasterxml.jackson.databind.JsonNode; import me.whereareiam.identica.conflict.resolver.ConflictResolver; -import me.whereareiam.identica.event.account.AccountPrepareEvent; import me.whereareiam.identica.model.conflict.ConflictContext; import me.whereareiam.identica.type.ConflictHook; -import me.whereareiam.identica.model.conflict.ConflictResolution; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -13,7 +12,7 @@ /** * Describes a conflict that can occur and how it should be handled. */ -public interface ConflictType { +public interface ConflictType { /** * Conflict key used in configuration and context. * @@ -65,32 +64,32 @@ default ConflictHook getDefaultHook() { } /** - * Build a conflict context during account preparation. + * Build a conflict context for the provided subject. * *
{@code
-	 * ConflictContext context = type.createContext(event);
+	 * ConflictContext context = type.createContext(subject);
 	 * if (context != null) {
 	 *     ConflictResolution resolution = conflictService.resolve(context);
-	 *     if (resolution != null) type.apply(event, context, resolution);
+	 *     if (resolution != null) {
+	 *         // interpret the resolution inside the owning conflict type module
+	 *     }
 	 * }
 	 * }
* - * @param event account prepare event + * @param subject conflict subject * @return conflict context or {@code null} if no conflict applies */ @Nullable - ConflictContext createContext(@NotNull AccountPrepareEvent event); + ConflictContext createContext(@NotNull S subject); /** - * Apply the resolved conflict decision to the event. + * Returns {@code true} when the provided rule selector applies to the current context. * - * @param event account prepare event * @param context conflict context - * @param resolution conflict resolution + * @param when opaque rule selector owned by the conflict type + * @return whether the rule should be selected */ - void apply( - @NotNull AccountPrepareEvent event, - @NotNull ConflictContext context, - @NotNull ConflictResolution resolution - ); + default boolean matchesRule(@NotNull ConflictContext context, @NotNull JsonNode when) { + return false; + } } diff --git a/identica-api/src/main/java/me/whereareiam/identica/database/AccountPersistenceService.java b/identica-api/src/main/java/me/whereareiam/identica/database/AccountPersistenceService.java index 98fbc92f9..48aafe41e 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/database/AccountPersistenceService.java +++ b/identica-api/src/main/java/me/whereareiam/identica/database/AccountPersistenceService.java @@ -1,7 +1,6 @@ package me.whereareiam.identica.database; import me.whereareiam.identica.model.identity.Account; -import me.whereareiam.identica.type.UsernameSource; import org.jetbrains.annotations.NotNull; import java.util.List; @@ -60,14 +59,6 @@ public interface AccountPersistenceService { */ void updateUsername(@NotNull UUID uniqueId, @NotNull String username); - /** - * Update username source. - * - * @param uniqueId account id - * @param source username source - */ - void updateUsernameSource(@NotNull UUID uniqueId, @NotNull UsernameSource source); - /** * Delete an account record. * diff --git a/identica-api/src/main/java/me/whereareiam/identica/database/UsernameHistoryPersistenceService.java b/identica-api/src/main/java/me/whereareiam/identica/database/UsernameHistoryPersistenceService.java deleted file mode 100644 index 4a59bd865..000000000 --- a/identica-api/src/main/java/me/whereareiam/identica/database/UsernameHistoryPersistenceService.java +++ /dev/null @@ -1,25 +0,0 @@ -package me.whereareiam.identica.database; - -import me.whereareiam.identica.model.UsernameHistoryEntry; -import org.jetbrains.annotations.NotNull; - -import java.util.UUID; - -/** - * Service for recording username changes. - */ -public interface UsernameHistoryPersistenceService { - /** - * Record a username change entry. - * - * @param entry username history entry to store - */ - void record(@NotNull UsernameHistoryEntry entry); - - /** - * Delete all username history for an account. - * - * @param uniqueId account id - */ - void deleteAll(@NotNull UUID uniqueId); -} diff --git a/identica-api/src/main/java/me/whereareiam/identica/event/account/AccountPrepareEvent.java b/identica-api/src/main/java/me/whereareiam/identica/event/account/AccountPrepareEvent.java deleted file mode 100644 index de89cb6c7..000000000 --- a/identica-api/src/main/java/me/whereareiam/identica/event/account/AccountPrepareEvent.java +++ /dev/null @@ -1,49 +0,0 @@ -package me.whereareiam.identica.event.account; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.Setter; -import lombok.ToString; -import me.whereareiam.identica.event.base.Event; -import me.whereareiam.identica.event.base.SynchronousEvent; -import me.whereareiam.identica.model.identity.Account; -import me.whereareiam.identica.model.identity.AccountDecision; -import me.whereareiam.identica.model.identity.provider.AccountProviderLink; -import me.whereareiam.identica.model.identity.provider.AccountProviderProfile; -import me.whereareiam.identica.model.provider.ProviderContext; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** - * Fired when account preparation has loaded or created core data - * and before a final account decision is set. - * - *
{@code
- * @IdenticEvent
- * public void onPrepare(AccountPrepareEvent event) {
- *     if (event.getDecision() == null) {
- *         event.setDecision(AccountDecision.allow());
- *     }
- * }
- * }
- */ -@Getter -@Setter -@ToString -@AllArgsConstructor -public class AccountPrepareEvent implements Event, SynchronousEvent { - /** - * Candidate internal username to apply; later listeners may overwrite this value. - * Defaults to the account username at the time of preparation. - */ - private @Nullable String effectiveUsername; - /** - * Provider context associated with this account preparation. - */ - private @Nullable ProviderContext provider; - - private final @NotNull Account account; - private final @NotNull AccountProviderLink link; - private final @NotNull AccountProviderProfile profile; - private @Nullable AccountDecision decision; -} diff --git a/identica-api/src/main/java/me/whereareiam/identica/event/pipeline/state/PipelineStateClearedEvent.java b/identica-api/src/main/java/me/whereareiam/identica/event/pipeline/state/PipelineStateClearedEvent.java index 7aebec50d..04992160c 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/event/pipeline/state/PipelineStateClearedEvent.java +++ b/identica-api/src/main/java/me/whereareiam/identica/event/pipeline/state/PipelineStateClearedEvent.java @@ -5,8 +5,8 @@ import lombok.ToString; import me.whereareiam.identica.event.base.Event; import me.whereareiam.identica.event.base.SynchronousEvent; -import me.whereareiam.identica.model.pipeline.state.PipelineState; -import me.whereareiam.identica.model.pipeline.state.PipelineStateReference; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.PipelineStateReference; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-api/src/main/java/me/whereareiam/identica/event/pipeline/state/PipelineStateSavedEvent.java b/identica-api/src/main/java/me/whereareiam/identica/event/pipeline/state/PipelineStateSavedEvent.java index 4b73ffd03..3cbd668ac 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/event/pipeline/state/PipelineStateSavedEvent.java +++ b/identica-api/src/main/java/me/whereareiam/identica/event/pipeline/state/PipelineStateSavedEvent.java @@ -5,8 +5,8 @@ import lombok.ToString; import me.whereareiam.identica.event.base.Event; import me.whereareiam.identica.event.base.SynchronousEvent; -import me.whereareiam.identica.model.pipeline.state.PipelineState; -import me.whereareiam.identica.model.pipeline.state.PipelineStateReference; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.PipelineStateReference; import org.jetbrains.annotations.NotNull; @Getter diff --git a/identica-api/src/main/java/me/whereareiam/identica/event/session/SessionOpenedEvent.java b/identica-api/src/main/java/me/whereareiam/identica/event/session/SessionOpenedEvent.java index 20bd0d26a..fb59b807c 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/event/session/SessionOpenedEvent.java +++ b/identica-api/src/main/java/me/whereareiam/identica/event/session/SessionOpenedEvent.java @@ -18,9 +18,4 @@ public class SessionOpenedEvent implements Event, SynchronousEvent { private final @NotNull UUID connectionUniqueId; private final @NotNull PipelineType pipelineType; private final @NotNull Session session; - /** - * Whether the authentication journey completed through framework - * recognition before the session was opened. - */ - private final boolean authenticationRecognized; } diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/config/Messages.java b/identica-api/src/main/java/me/whereareiam/identica/model/config/Messages.java index 235ae55dc..e810970a2 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/config/Messages.java +++ b/identica-api/src/main/java/me/whereareiam/identica/model/config/Messages.java @@ -67,7 +67,6 @@ public static class Commands { private @NotNull Migration migration; private @NotNull Reload reload; private @NotNull Admin admin; - private @NotNull Verification verification; /** * Configuration for enroll command messages. @@ -173,6 +172,7 @@ public static class Start { private @NotNull String pendingExists; private @NotNull String started; private @NotNull String providerUnsupported; + private @NotNull String providerUnavailable; } @Getter @@ -207,10 +207,8 @@ public static class Drop { public static class Admin { private @NotNull Clear clear; private @NotNull Delete delete; - private @NotNull ProviderRestriction providerRestriction; private @NotNull Reservation reservation; private @NotNull Sessions sessions; - private @NotNull Verification verification; @Getter @Setter @@ -346,79 +344,6 @@ public static class Delete { private @NotNull List disconnect; } - @Getter - @Setter - @ToString - public static class ProviderRestriction { - private @NotNull String enabled; - private @NotNull String disabled; - private @NotNull String enableFailed; - private @NotNull String providerNotFound; - private @NotNull Status status; - - @Getter - @Setter - @ToString - public static class Status { - /** - * Detailed provider restriction info. - * Placeholders: - * - {providerId} - * - {providerName} - * - {active} - * - {allow} - */ - private @NotNull List body; - private @NotNull String notFound; - @JsonProperty("list") - private @NotNull Listing list; - private @NotNull Labels labels; - - @Getter - @Setter - @ToString - public static class Labels { - private @NotNull String enabled; - private @NotNull String disabled; - } - - @Getter - @Setter - @ToString - public static class Listing { - /** - * Lines shown before listing provider restriction statuses. - * Placeholders: - * - {entries} - */ - private @NotNull List body; - private @NotNull Entries entries; - private @NotNull String empty; - - @Getter - @Setter - @ToString - public static class Entries { - /** - * Entry format for a single provider restriction status with populated allow entries. - * Placeholders: - * - {providerName} - * - {status} - * - {allow} - */ - private @NotNull String populated; - /** - * Entry format for a single provider restriction status with empty allow entries. - * Placeholders: - * - {providerName} - * - {status} - */ - private @NotNull String empty; - } - } - } - } - @Getter @Setter @ToString @@ -550,20 +475,6 @@ public static class Multiple { } } - @Getter - @Setter - @ToString - public static class Verification { - private @NotNull Reset reset; - - @Getter - @Setter - @ToString - public static class Reset { - private @NotNull String targetNotFound; - private @NotNull String completed; - } - } } /** * Configuration for reload command messages. @@ -588,114 +499,6 @@ public static class Reload { private @NotNull String error; } - @Getter - @Setter - @ToString - public static class Verification { - private @NotNull String playerOnly; - private @NotNull String notAllowed; - private @NotNull Methods methods; - private @NotNull Status status; - private @NotNull Enroll enroll; - private @NotNull Confirm confirm; - private @NotNull Use use; - private @NotNull Disable disable; - private @NotNull Cancel cancel; - - @Getter - @Setter - @ToString - public static class Status { - private @NotNull List body; - private @NotNull EntryFormat enrollmentEntry; - private @NotNull EntryFormat selectionEntry; - private @NotNull String emptyEnrollments; - private @NotNull String emptySelections; - } - - @Getter - @Setter - @ToString - public static class Enroll { - private @NotNull String unknownMethod; - private @NotNull String alreadyEnrolled; - } - - @Getter - @Setter - @ToString - public static class Confirm { - private @NotNull String noPending; - private @NotNull String invalidCode; - private @NotNull String protectedActionSelectionRequired; - private @NotNull String protectedActionSessionRequired; - private @NotNull String methodUnavailable; - private @NotNull String enabled; - private @NotNull String autoSelected; - private @NotNull RecoveryCodes recoveryCodes; - - @Getter - @Setter - @ToString - public static class RecoveryCodes { - private @NotNull Layout layout = Layout.TWO_COLUMN; - private @NotNull List body; - private @NotNull EntryFormat singleColumnEntry; - private @NotNull EntryFormat twoColumnEntry; - private @NotNull String empty; - - public enum Layout { - SINGLE_COLUMN, - TWO_COLUMN - } - } - } - - @Getter - @Setter - @ToString - public static class Use { - private @NotNull String providerNotFound; - private @NotNull String providerUnsupported; - private @NotNull String providerVerificationDisabled; - private @NotNull String methodNotEnrolled; - private @NotNull String methodDisabledForProvider; - private @NotNull String alreadySelected; - private @NotNull String updated; - } - - @Getter - @Setter - @ToString - public static class Disable { - private @NotNull String methodNotEnrolled; - private @NotNull String protectedPrompt; - private @NotNull String disabled; - } - - @Getter - @Setter - @ToString - public static class Cancel { - private @NotNull String noPending; - private @NotNull String cancelled; - private @NotNull String cancelledProtectedAction; - } - - @Getter - @Setter - @ToString - public static class Methods { - private @NotNull Totp totp; - - @Getter - @Setter - @ToString - public static class Totp { - private @NotNull List pending; - } - } - } /** * Format definitions for command entry rendering. @@ -721,21 +524,6 @@ public static class EntryFormat { public static class Providers { private @NotNull List noProvidersAvailable; private @NotNull List noProvidersMatched; - private @NotNull ProviderRestriction providerRestriction; - - @Getter - @Setter - @ToString - public static class ProviderRestriction { - /** - * Shared message sent when an active provider restriction denies a join. - * Placeholders: - * - {providerId} - * - {providerName} - * - {allow} - */ - private @NotNull List denied; - } } @Getter @@ -758,10 +546,6 @@ public static class Engine { @ToString public static class Prepare { private @NotNull List handshakeDenied; - /** - * Message shown when prepare can deny immediately due to an active provider restriction. - */ - private @NotNull List providerRestricted; private @NotNull Errors errors; @Getter diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/config/Replication.java b/identica-api/src/main/java/me/whereareiam/identica/model/config/Replication.java index 3b7f038c1..368830288 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/config/Replication.java +++ b/identica-api/src/main/java/me/whereareiam/identica/model/config/Replication.java @@ -45,10 +45,6 @@ public static class Redis { public static class Cache { private @NotNull String reservations; private @NotNull String instructions; - /** - * Cache namespace for provider join restriction runtime toggles. - */ - private @NotNull String providerJoinRestrictions; private @NotNull Delivery delivery; private @NotNull Sessions sessions; /** @@ -84,14 +80,6 @@ public static class Sessions { private @NotNull String user; private @NotNull String session; private @NotNull String subject; - private @NotNull Recognition recognition; - - @Getter - @Setter - @ToString - public static class Recognition { - private @NotNull String snapshot; - } } /** diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/config/Settings.java b/identica-api/src/main/java/me/whereareiam/identica/model/config/Settings.java index e31f3c9de..11c59c4c0 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/config/Settings.java +++ b/identica-api/src/main/java/me/whereareiam/identica/model/config/Settings.java @@ -12,12 +12,9 @@ import me.whereareiam.identica.model.sentinel.SentinelPolicy; import me.whereareiam.identica.type.identity.UniqueIdMode; import me.whereareiam.identica.type.session.SessionConcurrencyPolicy; -import me.whereareiam.identica.type.session.recognition.RecognitionSignal; import org.jetbrains.annotations.NotNull; import java.time.Duration; -import java.util.ArrayList; -import java.util.List; import java.util.Map; /** @@ -81,11 +78,6 @@ public static class Sessions { * Time-to-live used to keep live-session cache entries available while the player is online. */ private @NotNull Duration activeTtl; - /** - * Recognition policy for reconnecting players. - */ - private @NotNull Recognition recognition = new Recognition(); - /** * Returns active session TTL in milliseconds with validation. * @@ -98,71 +90,6 @@ public long activeTtlMillis() { return activeTtl.toMillis(); } - - /** - * Recognition behavior settings. - */ - @Getter - @Setter - @ToString - public static class Recognition { - /** - * Enables reconnect recognition for eligible providers. - */ - private boolean enabled; - /** - * Amount of time a stored recognition snapshot remains valid. - */ - private @NotNull Duration validity; - /** - * Default signal set used when providers do not override reconnect recognition signals. - */ - private @NotNull List defaultSignals = new ArrayList<>(); - /** - * Eligibility rules for whether recognition may be attempted. - */ - private @NotNull Eligibility eligibility = new Eligibility(); - - /** - * Returns recognition validity in milliseconds with validation. - * - * @return recognition validity in milliseconds - */ - public long validityMillis() { - if (validity.isZero() || validity.isNegative()) { - throw new IllegalStateException("settings.sessions.recognition.validity must be positive"); - } - - return validity.toMillis(); - } - - /** - * Eligibility rules for automatic reconnect recognition. - */ - @Getter - @Setter - @ToString - public static class Eligibility { - private @NotNull UntrustedIps untrustedIps = new UntrustedIps(); - - /** - * Untrusted-IP rules for whether automatic recognition may run. - */ - @Getter - @Setter - @ToString - public static class UntrustedIps { - /** - * Enables the untrusted-IP eligibility rule. - */ - private boolean enabled; - /** - * Exact IPs or CIDR ranges that suppress recognition eligibility. - */ - private @NotNull List entries = new ArrayList<>(); - } - } - } } /** diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/config/provider/Conflicts.java b/identica-api/src/main/java/me/whereareiam/identica/model/config/provider/Conflicts.java index 4919f48bd..98ded6c98 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/config/provider/Conflicts.java +++ b/identica-api/src/main/java/me/whereareiam/identica/model/config/provider/Conflicts.java @@ -32,16 +32,16 @@ public class Conflicts extends ConfigDocument { public static class ConflictRules { @JsonProperty("default") private @NotNull ConflictRule defaultRule; - private @NotNull List pairs = new ArrayList<>(); + private @NotNull List cases = new ArrayList<>(); /** - * Conflict rule for provider pairs or default scope. + * Conflict rule for a type-owned selector scope. */ @Getter @Setter @ToString public static class ConflictRule { - private @NotNull List providers = new ArrayList<>(); + private @NotNull JsonNode when = JsonNodeFactory.instance.objectNode(); private boolean force; private @NotNull List resolvers = new ArrayList<>(); diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/config/provider/Providers.java b/identica-api/src/main/java/me/whereareiam/identica/model/config/provider/Providers.java index 144a943a0..8b39cef58 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/config/provider/Providers.java +++ b/identica-api/src/main/java/me/whereareiam/identica/model/config/provider/Providers.java @@ -12,10 +12,7 @@ import me.whereareiam.configura.type.merge.tree.list.ListMode; import me.whereareiam.configura.type.merge.tree.list.ListPresence; import me.whereareiam.configura.type.merge.tree.list.ListUnknownEntries; -import me.whereareiam.identica.type.provider.ProviderJoinRestrictionCondition; import me.whereareiam.identica.type.session.SessionConcurrencyPolicy; -import me.whereareiam.identica.type.session.recognition.RecognitionSignal; -import me.whereareiam.identica.type.verification.UnavailableSelectionPolicy; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -51,10 +48,6 @@ public static class Behavior { * Time-to-live for provider attempt markers. */ private @NotNull Duration attemptTtl; - /** - * Time-to-live for provider join restriction runtime toggles. - */ - private @NotNull Duration joinRestrictionToggleTtl; /** * Returns attempt TTL in milliseconds with validation. @@ -69,18 +62,6 @@ public long attemptTtlMillis() { return attemptTtl.toMillis(); } - /** - * Returns join restriction toggle TTL in milliseconds with validation. - * - * @return join restriction toggle TTL in milliseconds - */ - public long joinRestrictionToggleTtlMillis() { - if (joinRestrictionToggleTtl.isZero() || joinRestrictionToggleTtl.isNegative()) { - throw new IllegalStateException("providers.behavior.joinRestrictionToggleTtl must be positive"); - } - - return joinRestrictionToggleTtl.toMillis(); - } } /** @@ -106,15 +87,15 @@ public static class ProviderEntry { @Merge(DeclaredObjectDefaults.class) @ExtendableDocument private @Nullable Session session; - /** - * Provider-specific restriction settings. - */ @Merge(DeclaredObjectDefaults.class) @ExtendableDocument - private @Nullable Restriction restriction; + private @Nullable Capabilities capabilities; + /** + * Feature-specific provider settings. + */ @Merge(DeclaredObjectDefaults.class) @ExtendableDocument - private @Nullable Verification verification; + private @Nullable Features features; /** * Hostnames (optionally with port) that map to this provider. * Entries must use the format {@code host} or {@code host:port}. @@ -130,86 +111,28 @@ public static class ProviderEntry { @ExtendableDocument public static class Session { private @Nullable SessionConcurrencyPolicy concurrencyPolicy; - @Merge(DeclaredObjectDefaults.class) - @ExtendableDocument - private @Nullable Recognition recognition; - - /** - * Provider-specific reconnect recognition settings. - */ - @Getter - @Setter - @ToString - @ExtendableDocument - public static class Recognition { - private @Nullable Boolean enabled; - private @NotNull List signals = new ArrayList<>(); - private boolean allowOnUntrustedIps; - } } /** - * Provider-specific restriction settings. + * Capability-specific provider settings. */ @Getter @Setter @ToString @ExtendableDocument - public static class Restriction { - /** - * Runtime-toggleable provider join restriction settings. - */ - @Merge(DeclaredObjectDefaults.class) - @ExtendableDocument - private @Nullable Join join; - - /** - * Runtime-toggleable provider join restriction settings. - */ - @Getter - @Setter - @ToString - @ExtendableDocument - public static class Join { - private boolean enabled; - private @Nullable List allow; - } + @PreserveUnknownFields + public static class Capabilities { } /** - * Shared verification settings for a provider. + * Feature-specific provider settings. */ @Getter @Setter @ToString @ExtendableDocument - public static class Verification { - private boolean enabled; - private boolean required; - private @Nullable UnavailableSelectionPolicy unavailableSelectionPolicy; - @Merge - @MergeList( - mode = ListMode.KEYED, - key = "id", - presence = ListPresence.DECLARED_ONLY, - unknownEntries = ListUnknownEntries.ALLOW - ) - private @NotNull List methods = new ArrayList<>(); - - /** - * Verification method entry for a provider. - */ - @Getter - @Setter - @ToString - @ExtendableDocument - public static class MethodEntry { - private @NotNull String id = ""; - private boolean enabled; - private int priority; - private @Nullable Boolean required; - private @Nullable UnavailableSelectionPolicy unavailableSelectionPolicy; - } + @PreserveUnknownFields + public static class Features { } } } diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/conflict/ConflictAttributeKey.java b/identica-api/src/main/java/me/whereareiam/identica/model/conflict/ConflictAttributeKey.java new file mode 100644 index 000000000..d8c0e384d --- /dev/null +++ b/identica-api/src/main/java/me/whereareiam/identica/model/conflict/ConflictAttributeKey.java @@ -0,0 +1,68 @@ +package me.whereareiam.identica.model.conflict; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; +import org.jetbrains.annotations.NotNull; + +/** + * Typed key used for conflict context and participant attributes. + * + * @param value type + */ +@Getter +@ToString +@EqualsAndHashCode +public final class ConflictAttributeKey { + private final @NotNull String id; + private final @NotNull Class type; + + /** + * Creates a new typed conflict attribute key. + * + * @param id stable attribute identifier + * @param type attribute type + */ + public ConflictAttributeKey(@NotNull String id, @NotNull Class type) { + if (id.isBlank()) + throw new IllegalArgumentException("attribute id cannot be blank"); + + this.id = id; + this.type = type; + } + + /** + * Creates a typed key for arbitrary object values. + * + * @param id stable attribute identifier + * @param type attribute type + * @param value type + * @return typed key + */ + public static @NotNull ConflictAttributeKey of( + @NotNull String id, + @NotNull Class type + ) { + return new ConflictAttributeKey<>(id, type); + } + + /** + * Creates a typed key for string values. + * + * @param id stable attribute identifier + * @return typed key + */ + public static @NotNull ConflictAttributeKey string(@NotNull String id) { + return of(id, String.class); + } + + /** + * Creates a typed key for boolean values. + * + * @param id stable attribute identifier + * @return typed key + */ + public static @NotNull ConflictAttributeKey bool(@NotNull String id) { + return of(id, Boolean.class); + } +} diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/conflict/ConflictContext.java b/identica-api/src/main/java/me/whereareiam/identica/model/conflict/ConflictContext.java index a657759a9..6b854784b 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/conflict/ConflictContext.java +++ b/identica-api/src/main/java/me/whereareiam/identica/model/conflict/ConflictContext.java @@ -1,14 +1,8 @@ package me.whereareiam.identica.model.conflict; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; -import me.whereareiam.identica.model.identity.Account; -import me.whereareiam.identica.model.identity.provider.AccountProviderLink; -import me.whereareiam.identica.model.identity.provider.AccountProviderProfile; +import lombok.*; +import me.whereareiam.identica.model.conflict.participant.ConflictParticipant; +import me.whereareiam.identica.model.conflict.participant.ConflictParticipantRole; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -32,49 +26,87 @@ public class ConflictContext { private @NotNull String key; /** - * Candidate value that is in conflict. + * Hook where the conflict is being evaluated. */ - private @NotNull String candidate; + private @NotNull String hook; - private @Nullable Account incomingAccount; - private @Nullable AccountProviderLink incomingLink; - private @Nullable AccountProviderProfile incomingProfile; - - private @Nullable Account existingAccount; - private @Nullable AccountProviderLink existingLink; - private @Nullable AccountProviderProfile existingProfile; + @Builder.Default + private @NotNull Map attributes = new HashMap<>(); - /** - * Additional eligibility-specific attributes. - */ @Builder.Default - private @NotNull Map extras = new HashMap<>(); + private @NotNull Map participants = new HashMap<>(); /** - * Read a typed extra value. + * Read a typed context attribute. * - * @param key extra key + * @param key attribute key * @param type expected type - * @return extra value or {@code null} + * @return typed value or {@code null} */ - public @Nullable T getExtra(@NotNull String key, @NotNull Class type) { - Object value = extras.get(key); - if (type.isInstance(value)) return type.cast(value); + public @Nullable T getAttribute(@NotNull ConflictAttributeKey key) { + Object value = attributes.get(key.getId()); + if (key.getType().isInstance(value)) return key.getType().cast(value); return null; } /** - * Store an extra value. + * Store a context attribute. * - * @param key extra key - * @param value extra value + * @param key attribute key + * @param value attribute value */ - public void putExtra(@NotNull String key, @Nullable Object value) { - if (key.isBlank()) return; + public void putAttribute(@NotNull ConflictAttributeKey key, @Nullable T value) { if (value == null) { - extras.remove(key); + attributes.remove(key.getId()); return; } - extras.put(key, value); + + attributes.put(key.getId(), value); + } + + /** + * Removes a context attribute. + * + * @param key attribute key + * @return {@code true} when removed + */ + public boolean removeAttribute(@NotNull ConflictAttributeKey key) { + return attributes.remove(key.getId()) != null; + } + + /** + * Returns a participant by role. + * + * @param role participant role + * @return participant or {@code null} + */ + public @Nullable ConflictParticipant getParticipant(@NotNull ConflictParticipantRole role) { + return participants.get(role); + } + + /** + * Read a typed participant attribute. + * + * @param role participant role + * @param key attribute key + * @param type expected type + * @return typed value or {@code null} + */ + public @Nullable T getParticipantAttribute( + @NotNull ConflictParticipantRole role, + @NotNull ConflictAttributeKey key + ) { + ConflictParticipant participant = participants.get(role); + if (participant == null) return null; + return participant.getAttribute(key); + } + + /** + * Store or replace a participant. + * + * @param participant participant to store + */ + public void putParticipant(@NotNull ConflictParticipant participant) { + participants.put(participant.getRole(), participant); } } diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/conflict/ConflictResolution.java b/identica-api/src/main/java/me/whereareiam/identica/model/conflict/ConflictResolution.java index a0584c83c..11dc18cb1 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/conflict/ConflictResolution.java +++ b/identica-api/src/main/java/me/whereareiam/identica/model/conflict/ConflictResolution.java @@ -6,6 +6,9 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.LinkedHashMap; +import java.util.Map; + /** * Result of conflict resolution. */ @@ -14,29 +17,17 @@ @RequiredArgsConstructor @SuppressWarnings("unused") public class ConflictResolution { - private final @NotNull Action action; - private final @Nullable String overrideValue; - private final @NotNull OverrideTarget overrideTarget; + private final @NotNull Decision decision; private final @Nullable String message; + private final @NotNull Map effects; /** - * Resolution actions. + * Resolution decisions. */ - public enum Action { + public enum Decision { ALLOW, PASS, - DENY, - KICK_EXISTING, - KICK_BOTH - } - - /** - * Targets for override values. - */ - public enum OverrideTarget { - INCOMING, - EXISTING, - BOTH + DENY } /** @@ -45,7 +36,7 @@ public enum OverrideTarget { * @return allow resolution */ public static ConflictResolution allow() { - return new ConflictResolution(Action.ALLOW, null, OverrideTarget.INCOMING, null); + return new ConflictResolution(Decision.ALLOW, null, Map.of()); } /** @@ -54,60 +45,54 @@ public static ConflictResolution allow() { * @return pass resolution */ public static ConflictResolution pass() { - return new ConflictResolution(Action.PASS, null, OverrideTarget.INCOMING, null); + return new ConflictResolution(Decision.PASS, null, Map.of()); } /** - * Allow the candidate value with an override. + * Deny the operation. * - * @param overrideValue replacement value to apply - * @return allow resolution with override + * @param message optional message + * @return deny resolution */ - public static ConflictResolution allowWithOverride(@Nullable String overrideValue) { - return new ConflictResolution(Action.ALLOW, overrideValue, OverrideTarget.INCOMING, null); + public static ConflictResolution deny(@Nullable String message) { + return new ConflictResolution(Decision.DENY, message, Map.of()); } /** - * Allow the candidate value with an override. + * Read a typed effect value. * - * @param overrideValue replacement value to apply - * @param target target to apply the override to - * @return allow resolution with override + * @param key effect key + * @param type expected type + * @return typed effect or {@code null} */ - public static ConflictResolution allowWithOverride( - @Nullable String overrideValue, - @NotNull OverrideTarget target - ) { - return new ConflictResolution(Action.ALLOW, overrideValue, target, null); + public @Nullable T getEffect(@NotNull String key, @NotNull Class type) { + Object value = effects.get(key); + if (type.isInstance(value)) return type.cast(value); + return null; } /** - * Deny the operation. + * Returns {@code true} when this resolution contains the given effect. * - * @param message optional message - * @return deny resolution + * @param key effect key + * @return whether the effect is present */ - public static ConflictResolution deny(@Nullable String message) { - return new ConflictResolution(Action.DENY, null, OverrideTarget.INCOMING, message); + public boolean hasEffect(@NotNull String key) { + return effects.containsKey(key); } /** - * Kick the existing session and allow the joiner. + * Return a copy of this resolution with an additional effect. * - * @param overrideValue optional override value - * @return kick-existing resolution + * @param key effect key + * @param value effect value + * @return copied resolution */ - public static ConflictResolution kickExisting(@Nullable String overrideValue) { - return new ConflictResolution(Action.KICK_EXISTING, overrideValue, OverrideTarget.INCOMING, null); - } + public @NotNull ConflictResolution withEffect(@NotNull String key, @Nullable Object value) { + if (key.isBlank() || value == null) return this; - /** - * Kick both sessions and deny the joiner. - * - * @param message optional message - * @return kick-both resolution - */ - public static ConflictResolution kickBoth(@Nullable String message) { - return new ConflictResolution(Action.KICK_BOTH, null, OverrideTarget.INCOMING, message); + Map updated = new LinkedHashMap<>(effects); + updated.put(key, value); + return new ConflictResolution(decision, message, Map.copyOf(updated)); } } diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/conflict/participant/ConflictParticipant.java b/identica-api/src/main/java/me/whereareiam/identica/model/conflict/participant/ConflictParticipant.java new file mode 100644 index 000000000..6f8a1bd21 --- /dev/null +++ b/identica-api/src/main/java/me/whereareiam/identica/model/conflict/participant/ConflictParticipant.java @@ -0,0 +1,70 @@ +package me.whereareiam.identica.model.conflict.participant; + +import lombok.*; +import me.whereareiam.identica.model.conflict.ConflictAttributeKey; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; + +/** + * Generic participant involved in a conflict. + */ +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Builder(toBuilder = true) +@SuppressWarnings("unused") +public class ConflictParticipant { + /** + * Participant role identifier such as {@code incoming} or {@code existing}. + */ + private @NotNull ConflictParticipantRole role; + + /** + * Arbitrary participant attributes. + */ + @Builder.Default + private @NotNull Map attributes = new HashMap<>(); + + /** + * Read a typed participant attribute. + * + * @param key attribute key + * @param type expected type + * @return typed value or {@code null} + */ + public @Nullable T getAttribute(@NotNull ConflictAttributeKey key) { + Object value = attributes.get(key.getId()); + if (key.getType().isInstance(value)) return key.getType().cast(value); + return null; + } + + /** + * Store a participant attribute. + * + * @param key attribute key + * @param value attribute value + */ + public void putAttribute(@NotNull ConflictAttributeKey key, @Nullable T value) { + if (value == null) { + attributes.remove(key.getId()); + return; + } + + attributes.put(key.getId(), value); + } + + /** + * Removes a participant attribute. + * + * @param key attribute key + * @return {@code true} when removed + */ + public boolean removeAttribute(@NotNull ConflictAttributeKey key) { + return attributes.remove(key.getId()) != null; + } +} diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/conflict/participant/ConflictParticipantRole.java b/identica-api/src/main/java/me/whereareiam/identica/model/conflict/participant/ConflictParticipantRole.java new file mode 100644 index 000000000..5f38f457d --- /dev/null +++ b/identica-api/src/main/java/me/whereareiam/identica/model/conflict/participant/ConflictParticipantRole.java @@ -0,0 +1,37 @@ +package me.whereareiam.identica.model.conflict.participant; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; +import org.jetbrains.annotations.NotNull; + +/** + * Typed participant role used by conflict contexts. + */ +@Getter +@ToString +@EqualsAndHashCode +public final class ConflictParticipantRole { + private final @NotNull String id; + + /** + * Creates a new conflict participant role. + * + * @param id stable role identifier + */ + public ConflictParticipantRole(@NotNull String id) { + if (id.isBlank()) throw new IllegalArgumentException("role id cannot be blank"); + + this.id = id; + } + + /** + * Creates a role from a stable identifier. + * + * @param id stable role identifier + * @return typed role + */ + public static @NotNull ConflictParticipantRole of(@NotNull String id) { + return new ConflictParticipantRole(id); + } +} diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/delivery/DeliveryPayload.java b/identica-api/src/main/java/me/whereareiam/identica/model/delivery/DeliveryPayload.java index 873179878..3965983fe 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/delivery/DeliveryPayload.java +++ b/identica-api/src/main/java/me/whereareiam/identica/model/delivery/DeliveryPayload.java @@ -39,6 +39,5 @@ public static class CompletionPayload { private @Nullable UUID connectionUniqueId; private @Nullable UUID accountUniqueId; private @Nullable PipelineType pipelineType; - private boolean authenticationRecognized; } } diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/identity/Account.java b/identica-api/src/main/java/me/whereareiam/identica/model/identity/Account.java index b1d43a37f..4a1112ee8 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/identity/Account.java +++ b/identica-api/src/main/java/me/whereareiam/identica/model/identity/Account.java @@ -1,12 +1,6 @@ package me.whereareiam.identica.model.identity; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; -import me.whereareiam.identica.type.UsernameSource; +import lombok.*; import org.jetbrains.annotations.NotNull; import java.util.UUID; @@ -30,11 +24,6 @@ public class Account { */ private @NotNull String username; - /** - * Source of the currently active username ("eligibility", "manual", "system"). - */ - private @NotNull UsernameSource source; - private long createdAt; private long lastSeenAt; } diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/pipeline/PipelineResult.java b/identica-api/src/main/java/me/whereareiam/identica/model/pipeline/PipelineResult.java index 63d01b2a4..dfbeab809 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/pipeline/PipelineResult.java +++ b/identica-api/src/main/java/me/whereareiam/identica/model/pipeline/PipelineResult.java @@ -3,7 +3,7 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; import me.whereareiam.identica.model.pipeline.journey.stage.step.StepResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.PipelineState; import me.whereareiam.identica.type.pipeline.PipelineStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/pipeline/authentication/AuthenticationOutcomeItem.java b/identica-api/src/main/java/me/whereareiam/identica/model/pipeline/authentication/AuthenticationOutcomeItem.java deleted file mode 100644 index afafc1824..000000000 --- a/identica-api/src/main/java/me/whereareiam/identica/model/pipeline/authentication/AuthenticationOutcomeItem.java +++ /dev/null @@ -1,26 +0,0 @@ -package me.whereareiam.identica.model.pipeline.authentication; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.ToString; -import me.whereareiam.identica.pipeline.state.PipelineStateItem; -import org.jetbrains.annotations.NotNull; - -/** - * Pipeline state item describing how an authentication journey completed. - */ -@Getter -@ToString -@NoArgsConstructor -@AllArgsConstructor -public final class AuthenticationOutcomeItem implements PipelineStateItem { - private @NotNull AuthenticationOutcome outcome; - - /** - * Authentication outcomes emitted by the framework authentication journey. - */ - public enum AuthenticationOutcome { - RECOGNIZED - } -} diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/pipeline/completion/CompletionContext.java b/identica-api/src/main/java/me/whereareiam/identica/model/pipeline/completion/CompletionContext.java index 8adcee0ed..6d288ae4a 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/pipeline/completion/CompletionContext.java +++ b/identica-api/src/main/java/me/whereareiam/identica/model/pipeline/completion/CompletionContext.java @@ -20,11 +20,6 @@ public class CompletionContext { private @NotNull PipelineType pipelineType; private @NotNull Session session; private @Nullable InternalProvider provider; - /** - * Whether the authentication journey completed through framework - * recognition before this completion context was built. - */ - private boolean authenticationRecognized; public @Nullable UUID getAccountUniqueId() { return session.getUniqueId(); diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/pipeline/completion/CompletionPendingState.java b/identica-api/src/main/java/me/whereareiam/identica/model/pipeline/completion/CompletionPendingState.java index 2c7edbf7c..55607e82a 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/pipeline/completion/CompletionPendingState.java +++ b/identica-api/src/main/java/me/whereareiam/identica/model/pipeline/completion/CompletionPendingState.java @@ -16,11 +16,6 @@ public class CompletionPendingState { private @NotNull PipelineType pipelineType; private @Nullable UUID connectionUniqueId; private @Nullable UUID accountUniqueId; - /** - * Whether the authentication journey completed through framework - * recognition before completion messaging is rendered. - */ - private boolean authenticationRecognized; public static class CompletionPendingStateBuilder { public @NotNull CompletionPendingStateBuilder identicaUniqueId(@Nullable UUID accountUniqueId) { diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/pipeline/journey/JourneyRuleContext.java b/identica-api/src/main/java/me/whereareiam/identica/model/pipeline/journey/JourneyRuleContext.java index 2fdd30bae..8bbdef88f 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/pipeline/journey/JourneyRuleContext.java +++ b/identica-api/src/main/java/me/whereareiam/identica/model/pipeline/journey/JourneyRuleContext.java @@ -5,7 +5,7 @@ import lombok.NoArgsConstructor; import lombok.Setter; import me.whereareiam.identica.pipeline.ScenarioContext; -import me.whereareiam.identica.model.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.PipelineState; import me.whereareiam.identica.type.pipeline.PipelineType; import me.whereareiam.identica.type.pipeline.journey.JourneyMode; import org.jetbrains.annotations.NotNull; diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/provider/InternalProvider.java b/identica-api/src/main/java/me/whereareiam/identica/model/provider/InternalProvider.java index 90b325d00..cab2c3b74 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/provider/InternalProvider.java +++ b/identica-api/src/main/java/me/whereareiam/identica/model/provider/InternalProvider.java @@ -4,6 +4,7 @@ import lombok.Getter; import lombok.Setter; import me.whereareiam.identica.provider.IdenticaProvider; +import me.whereareiam.identica.provider.capability.contribution.ProviderCapabilityContribution; import me.whereareiam.identica.provider.eligibility.ProviderEligibilityResolver; import me.whereareiam.identica.provider.migration.ProviderMigrationPrecheck; import me.whereareiam.identica.provider.profile.ProfileSubjectResolver; @@ -65,4 +66,8 @@ public class InternalProvider { * Migration prechecks registered for this provider. */ private @Nullable Set migrationPrechecks; + /** + * Capability contributions registered for this provider. + */ + private @Nullable Set capabilityContributions; } diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/provider/ProviderDescriptor.java b/identica-api/src/main/java/me/whereareiam/identica/model/provider/ProviderDescriptor.java index 589f1c5b4..4ba8b9254 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/provider/ProviderDescriptor.java +++ b/identica-api/src/main/java/me/whereareiam/identica/model/provider/ProviderDescriptor.java @@ -4,12 +4,14 @@ import lombok.Setter; import lombok.ToString; import me.whereareiam.identica.model.provider.dependency.ProviderLibraries; -import me.whereareiam.identica.type.provider.ProviderCapability; +import me.whereareiam.identica.type.provider.ProviderFeature; +import me.whereareiam.identica.type.provider.capability.ProviderCapability; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; +import java.util.Locale; /** * Descriptor for a provider module. @@ -28,9 +30,13 @@ public class ProviderDescriptor { private @NotNull List supportedPlatforms; /** - * Provider capability ids. + * Declared capability ids resolved from provider-supplied bootstraps. */ - private List capabilities = new ArrayList<>(); + private @NotNull List declaredCapabilityIds = new ArrayList<>(); + /** + * Declared feature ids resolved from provider-supplied declarations. + */ + private @NotNull List declaredFeatureIds = new ArrayList<>(); private int priority = 0; @@ -44,9 +50,9 @@ public class ProviderDescriptor { */ public boolean hasCapability(@Nullable ProviderCapability capability) { if (capability == null) return false; - if (capabilities == null || capabilities.isEmpty()) return false; - for (ProviderCapability entry : capabilities) - if (entry == capability) return true; + for (String entry : declaredCapabilityIds) + if (capability.matches(entry)) + return true; return false; } @@ -58,6 +64,47 @@ public boolean hasCapability(@Nullable ProviderCapability capability) { * @return {@code true} when the id is listed */ public boolean hasCapabilityId(@Nullable String capabilityId) { - return hasCapability(ProviderCapability.fromId(capabilityId)); + if (capabilityId == null || capabilityId.isBlank() || declaredCapabilityIds.isEmpty()) return false; + + String normalized = capabilityId.trim().toLowerCase(Locale.ROOT); + for (String entry : declaredCapabilityIds) { + if (entry == null || entry.isBlank()) continue; + if (normalized.equals(entry.trim().toLowerCase(Locale.ROOT))) + return true; + } + + return false; + } + + /** + * Checks whether the provider advertises the feature. + * + * @param feature feature to check + * @return {@code true} when the feature is listed + */ + public boolean hasFeature(@Nullable ProviderFeature feature) { + if (feature == null) return false; + for (String entry : declaredFeatureIds) + if (feature.matches(entry)) return true; + + return false; + } + + /** + * Checks whether the provider advertises a feature id. + * + * @param featureId feature id to check + * @return {@code true} when the id is listed + */ + public boolean hasFeatureId(@Nullable String featureId) { + if (featureId == null || featureId.isBlank() || declaredFeatureIds.isEmpty()) return false; + + String normalized = featureId.trim().toLowerCase(Locale.ROOT); + for (String entry : declaredFeatureIds) { + if (entry == null || entry.isBlank()) continue; + if (normalized.equals(entry.trim().toLowerCase(Locale.ROOT))) return true; + } + + return false; } } diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/provider/capability/ProviderCapabilityDeclaration.java b/identica-api/src/main/java/me/whereareiam/identica/model/provider/capability/ProviderCapabilityDeclaration.java new file mode 100644 index 000000000..7343aedec --- /dev/null +++ b/identica-api/src/main/java/me/whereareiam/identica/model/provider/capability/ProviderCapabilityDeclaration.java @@ -0,0 +1,23 @@ +package me.whereareiam.identica.model.provider.capability; + +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; +import me.whereareiam.identica.type.provider.capability.ProviderCapability; +import me.whereareiam.identica.type.provider.capability.ProviderCapabilityScope; +import org.jetbrains.annotations.NotNull; + +import java.util.Set; + +/** + * Declared capability metadata exposed by a provider bootstrap. + */ +@Getter +@ToString +@Builder +public final class ProviderCapabilityDeclaration { + private final @NotNull ProviderCapability capability; + @Builder.Default + private final @NotNull Set scopes = Set.of(); + private final boolean requiresContribution; +} diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/provider/capability/ProviderCapabilityInstallation.java b/identica-api/src/main/java/me/whereareiam/identica/model/provider/capability/ProviderCapabilityInstallation.java new file mode 100644 index 000000000..a54925923 --- /dev/null +++ b/identica-api/src/main/java/me/whereareiam/identica/model/provider/capability/ProviderCapabilityInstallation.java @@ -0,0 +1,20 @@ +package me.whereareiam.identica.model.provider.capability; + +import com.google.inject.Injector; +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; +import me.whereareiam.identica.provider.capability.bootstrap.ProviderCapabilityBootstrap; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Installed runtime state for a capability bootstrap. + */ +@Getter +@ToString +@Builder +public final class ProviderCapabilityInstallation { + private final @NotNull ProviderCapabilityBootstrap bootstrap; + private final @Nullable Injector globalInjector; +} diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/provider/restriction/ProviderJoinRestrictionDecision.java b/identica-api/src/main/java/me/whereareiam/identica/model/provider/restriction/ProviderJoinRestrictionDecision.java deleted file mode 100644 index f333f549d..000000000 --- a/identica-api/src/main/java/me/whereareiam/identica/model/provider/restriction/ProviderJoinRestrictionDecision.java +++ /dev/null @@ -1,25 +0,0 @@ -package me.whereareiam.identica.model.provider.restriction; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.ToString; -import me.whereareiam.identica.type.provider.ProviderJoinRestrictionCondition; -import org.jetbrains.annotations.NotNull; - -import java.util.Set; - -/** - * Resolved join restriction decision for a provider join attempt. - */ -@Getter -@ToString -@AllArgsConstructor -@Builder(toBuilder = true) -public class ProviderJoinRestrictionDecision { - private final boolean allowed; - private final boolean configured; - private final boolean active; - private final @NotNull Set allow; - private final @NotNull Set matchedConditions; -} diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/provider/restriction/ProviderJoinRestrictionStatus.java b/identica-api/src/main/java/me/whereareiam/identica/model/provider/restriction/ProviderJoinRestrictionStatus.java deleted file mode 100644 index d2c8944ae..000000000 --- a/identica-api/src/main/java/me/whereareiam/identica/model/provider/restriction/ProviderJoinRestrictionStatus.java +++ /dev/null @@ -1,24 +0,0 @@ -package me.whereareiam.identica.model.provider.restriction; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.ToString; -import me.whereareiam.identica.type.provider.ProviderJoinRestrictionCondition; -import org.jetbrains.annotations.NotNull; - -import java.util.Set; - -/** - * Resolved provider join restriction status. - */ -@Getter -@Builder(toBuilder = true) -@ToString -@AllArgsConstructor -public class ProviderJoinRestrictionStatus { - private final @NotNull String providerId; - private final boolean active; - - private final @NotNull Set allow; -} diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/verification/VerificationDisableResult.java b/identica-api/src/main/java/me/whereareiam/identica/model/verification/VerificationDisableResult.java deleted file mode 100644 index 703c50edf..000000000 --- a/identica-api/src/main/java/me/whereareiam/identica/model/verification/VerificationDisableResult.java +++ /dev/null @@ -1,23 +0,0 @@ -package me.whereareiam.identica.model.verification; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; -import me.whereareiam.identica.type.verification.status.VerificationDisableStatus; - -/** - * Result payload for verification method disable operations. - */ -@Getter -@Setter -@ToString -@NoArgsConstructor -@AllArgsConstructor -@Builder(toBuilder = true) -public class VerificationDisableResult { - private VerificationDisableStatus status; - private String methodId; -} diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/verification/VerificationResetResult.java b/identica-api/src/main/java/me/whereareiam/identica/model/verification/VerificationResetResult.java deleted file mode 100644 index b627216c7..000000000 --- a/identica-api/src/main/java/me/whereareiam/identica/model/verification/VerificationResetResult.java +++ /dev/null @@ -1,23 +0,0 @@ -package me.whereareiam.identica.model.verification; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; -import me.whereareiam.identica.type.verification.status.VerificationResetStatus; - -/** - * Result payload for verification reset operations. - */ -@Getter -@Setter -@ToString -@NoArgsConstructor -@AllArgsConstructor -@Builder(toBuilder = true) -public class VerificationResetResult { - private VerificationResetStatus status; - private String providerId; -} diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/verification/challenge/VerificationChallengeState.java b/identica-api/src/main/java/me/whereareiam/identica/model/verification/challenge/VerificationChallengeState.java deleted file mode 100644 index 0b733fe1f..000000000 --- a/identica-api/src/main/java/me/whereareiam/identica/model/verification/challenge/VerificationChallengeState.java +++ /dev/null @@ -1,9 +0,0 @@ -package me.whereareiam.identica.model.verification.challenge; - -import me.whereareiam.identica.model.verification.process.VerificationProcessState; - -/** - * Marker for method-owned challenge state. - */ -public interface VerificationChallengeState extends VerificationProcessState { -} diff --git a/identica-api/src/main/java/me/whereareiam/identica/pipeline/PipelineGroup.java b/identica-api/src/main/java/me/whereareiam/identica/pipeline/PipelineGroup.java index ffc17b71c..ddbb8c174 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/pipeline/PipelineGroup.java +++ b/identica-api/src/main/java/me/whereareiam/identica/pipeline/PipelineGroup.java @@ -2,7 +2,7 @@ import me.whereareiam.identica.model.pipeline.GroupOutcome; import me.whereareiam.identica.model.pipeline.PipelineResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.PipelineState; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-api/src/main/java/me/whereareiam/identica/pipeline/PipelinePhase.java b/identica-api/src/main/java/me/whereareiam/identica/pipeline/PipelinePhase.java index 17292a459..4ab5ee382 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/pipeline/PipelinePhase.java +++ b/identica-api/src/main/java/me/whereareiam/identica/pipeline/PipelinePhase.java @@ -1,7 +1,7 @@ package me.whereareiam.identica.pipeline; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.model.pipeline.phase.PhaseResult; +import me.whereareiam.identica.pipeline.state.PipelineState; import org.jetbrains.annotations.NotNull; import java.util.concurrent.CompletionStage; diff --git a/identica-api/src/main/java/me/whereareiam/identica/pipeline/PipelineRegistry.java b/identica-api/src/main/java/me/whereareiam/identica/pipeline/PipelineRegistry.java index f155d5c7e..c0c6b02cd 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/pipeline/PipelineRegistry.java +++ b/identica-api/src/main/java/me/whereareiam/identica/pipeline/PipelineRegistry.java @@ -1,7 +1,7 @@ package me.whereareiam.identica.pipeline; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.model.pipeline.phase.PhasePlacement; +import me.whereareiam.identica.pipeline.state.PipelineState; import org.jetbrains.annotations.NotNull; import java.util.List; diff --git a/identica-api/src/main/java/me/whereareiam/identica/pipeline/extension/PipelineExtensionBuilder.java b/identica-api/src/main/java/me/whereareiam/identica/pipeline/extension/PipelineExtensionBuilder.java index f921717da..475848eb2 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/pipeline/extension/PipelineExtensionBuilder.java +++ b/identica-api/src/main/java/me/whereareiam/identica/pipeline/extension/PipelineExtensionBuilder.java @@ -1,12 +1,11 @@ package me.whereareiam.identica.pipeline.extension; -import me.whereareiam.identica.pipeline.PipelineGroup; -import me.whereareiam.identica.model.pipeline.phase.PhasePlacement; -import me.whereareiam.identica.pipeline.PipelinePhase; import me.whereareiam.identica.model.pipeline.journey.stage.JourneyStage; import me.whereareiam.identica.model.pipeline.journey.stage.step.JourneyStep; +import me.whereareiam.identica.model.pipeline.phase.PhasePlacement; +import me.whereareiam.identica.pipeline.PipelineGroup; +import me.whereareiam.identica.pipeline.PipelinePhase; import me.whereareiam.identica.pipeline.journey.step.Step; -import me.whereareiam.identica.type.pipeline.PipelineScope; import me.whereareiam.identica.type.pipeline.PipelineType; import me.whereareiam.identica.type.pipeline.journey.JourneyMode; import me.whereareiam.identica.type.pipeline.journey.StageType; @@ -20,24 +19,76 @@ */ @SuppressWarnings("unused") public interface PipelineExtensionBuilder { - void registerGroup(@NotNull PipelineScope scope, @NotNull PipelineGroup group); + /** + * Registers a prepare pipeline group. + * + * @param group prepare pipeline group to register + */ + void registerPrepareGroup(@NotNull PipelineGroup group); - void registerPhase( - @NotNull PipelineScope scope, + /** + * Registers a phase in the prepare pipeline. + * + * @param groupId target prepare group identifier + * @param phase prepare phase to register + * @param placement placement relative to existing phases + */ + void registerPreparePhase( @NotNull String groupId, @NotNull PipelinePhase phase, @NotNull PhasePlacement placement ); - void registerStage(@NotNull PipelineScope scope, @NotNull JourneyStage stage); + /** + * Registers a scenario pipeline group. + * + * @param pipelineType scenario pipeline to register into + * @param group scenario pipeline group to register + */ + void registerScenarioGroup(@NotNull PipelineType pipelineType, @NotNull PipelineGroup group); + /** + * Registers a phase in a scenario pipeline. + * + * @param pipelineType scenario pipeline to register into + * @param groupId target scenario group identifier + * @param phase scenario phase to register + * @param placement placement relative to existing phases + */ + void registerScenarioPhase( + @NotNull PipelineType pipelineType, + @NotNull String groupId, + @NotNull PipelinePhase phase, + @NotNull PhasePlacement placement + ); + + /** + * Registers a stage for a scenario pipeline journey. + * + * @param pipelineType scenario pipeline to register into + * @param stage journey stage to register + */ + void registerStage(@NotNull PipelineType pipelineType, @NotNull JourneyStage stage); + + /** + * Registers a journey step using the scenarios declared on the step itself. + * + * @param step journey step to register + */ void registerStep(@NotNull JourneyStep step); + /** + * Registers a journey step for a scenario pipeline using the step's default journey modes. + * + * @param pipelineType scenario pipeline to register into + * @param providerId provider identifier, if the step is provider-specific + * @param stageType target journey stage type + * @param step step to register + */ default void registerStep( - @NotNull PipelineScope scope, + @NotNull PipelineType pipelineType, @Nullable String providerId, @NotNull StageType stageType, - @NotNull PipelineType pipelineType, @NotNull Step step ) { registerStep(JourneyStep.builder() @@ -49,11 +100,19 @@ default void registerStep( .build()); } + /** + * Registers a journey step for a specific journey mode in a scenario pipeline. + * + * @param pipelineType scenario pipeline to register into + * @param providerId provider identifier, if the step is provider-specific + * @param stageType target journey stage type + * @param journeyMode target journey mode + * @param step step to register + */ default void registerStep( - @NotNull PipelineScope scope, + @NotNull PipelineType pipelineType, @Nullable String providerId, @NotNull StageType stageType, - @NotNull PipelineType pipelineType, @NotNull JourneyMode journeyMode, @NotNull Step step ) { @@ -72,8 +131,8 @@ default void registerStep( @NotNull StageType stageType, @NotNull Step step ) { - registerStep(PipelineScope.AUTHENTICATION, providerId, stageType, PipelineType.AUTHENTICATION, step); - registerStep(PipelineScope.REGISTRATION, providerId, stageType, PipelineType.REGISTRATION, step); + registerStep(PipelineType.AUTHENTICATION, providerId, stageType, step); + registerStep(PipelineType.REGISTRATION, providerId, stageType, step); } default void registerStep( @@ -82,8 +141,8 @@ default void registerStep( @NotNull JourneyMode journeyMode, @NotNull Step step ) { - registerStep(PipelineScope.AUTHENTICATION, providerId, stageType, PipelineType.AUTHENTICATION, journeyMode, step); - registerStep(PipelineScope.REGISTRATION, providerId, stageType, PipelineType.REGISTRATION, journeyMode, step); + registerStep(PipelineType.AUTHENTICATION, providerId, stageType, journeyMode, step); + registerStep(PipelineType.REGISTRATION, providerId, stageType, journeyMode, step); } } diff --git a/identica-api/src/main/java/me/whereareiam/identica/pipeline/journey/step/type/AuthenticationRecognitionStep.java b/identica-api/src/main/java/me/whereareiam/identica/pipeline/journey/step/type/AuthenticationRecognitionStep.java deleted file mode 100644 index ead09e0e3..000000000 --- a/identica-api/src/main/java/me/whereareiam/identica/pipeline/journey/step/type/AuthenticationRecognitionStep.java +++ /dev/null @@ -1,9 +0,0 @@ -package me.whereareiam.identica.pipeline.journey.step.type; - -import me.whereareiam.identica.pipeline.journey.step.Step; - -/** - * Marker for authentication steps that complete by framework recognition. - */ -public interface AuthenticationRecognitionStep extends Step { -} diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/AbstractGroupState.java b/identica-api/src/main/java/me/whereareiam/identica/pipeline/state/AbstractGroupState.java similarity index 80% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/AbstractGroupState.java rename to identica-api/src/main/java/me/whereareiam/identica/pipeline/state/AbstractGroupState.java index df135e35b..25fef3b9e 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/AbstractGroupState.java +++ b/identica-api/src/main/java/me/whereareiam/identica/pipeline/state/AbstractGroupState.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.engine.pipeline.scenario.base; +package me.whereareiam.identica.pipeline.state; import lombok.Getter; import lombok.Setter; diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/pipeline/state/PipelineState.java b/identica-api/src/main/java/me/whereareiam/identica/pipeline/state/PipelineState.java similarity index 97% rename from identica-api/src/main/java/me/whereareiam/identica/model/pipeline/state/PipelineState.java rename to identica-api/src/main/java/me/whereareiam/identica/pipeline/state/PipelineState.java index 630090787..2bd0e8e94 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/pipeline/state/PipelineState.java +++ b/identica-api/src/main/java/me/whereareiam/identica/pipeline/state/PipelineState.java @@ -1,15 +1,14 @@ -package me.whereareiam.identica.model.pipeline.state; +package me.whereareiam.identica.pipeline.state; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; -import me.whereareiam.identica.model.pipeline.PipelineCursor; -import me.whereareiam.identica.replication.codec.SnapshotCodec; import me.whereareiam.identica.model.auth.AuthContext; import me.whereareiam.identica.model.migration.MigrationContext; -import me.whereareiam.identica.pipeline.state.PipelineStateItem; -import me.whereareiam.identica.pipeline.ScenarioContext; +import me.whereareiam.identica.model.pipeline.PipelineCursor; import me.whereareiam.identica.model.registration.RegistrationContext; +import me.whereareiam.identica.pipeline.ScenarioContext; +import me.whereareiam.identica.replication.codec.SnapshotCodec; import me.whereareiam.identica.type.pipeline.PipelineType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/pipeline/state/PipelineStateReference.java b/identica-api/src/main/java/me/whereareiam/identica/pipeline/state/PipelineStateReference.java similarity index 98% rename from identica-api/src/main/java/me/whereareiam/identica/model/pipeline/state/PipelineStateReference.java rename to identica-api/src/main/java/me/whereareiam/identica/pipeline/state/PipelineStateReference.java index 1157f0e69..f850a4e40 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/pipeline/state/PipelineStateReference.java +++ b/identica-api/src/main/java/me/whereareiam/identica/pipeline/state/PipelineStateReference.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.model.pipeline.state; +package me.whereareiam.identica.pipeline.state; import lombok.*; import me.whereareiam.identica.identity.actor.ConnectionIdentity; diff --git a/identica-api/src/main/java/me/whereareiam/identica/pipeline/state/PipelineStateStore.java b/identica-api/src/main/java/me/whereareiam/identica/pipeline/state/PipelineStateStore.java index 46c555793..caf7f2175 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/pipeline/state/PipelineStateStore.java +++ b/identica-api/src/main/java/me/whereareiam/identica/pipeline/state/PipelineStateStore.java @@ -1,9 +1,7 @@ package me.whereareiam.identica.pipeline.state; -import me.whereareiam.identica.model.auth.request.ResumeRequest; import me.whereareiam.identica.model.auth.request.AdvanceRequest; -import me.whereareiam.identica.model.pipeline.state.PipelineState; -import me.whereareiam.identica.model.pipeline.state.PipelineStateReference; +import me.whereareiam.identica.model.auth.request.ResumeRequest; import me.whereareiam.identica.pipeline.ScenarioContext; import org.jetbrains.annotations.NotNull; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/CompletionPipelineState.java b/identica-api/src/main/java/me/whereareiam/identica/pipeline/state/completion/CompletionPipelineState.java similarity index 81% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/CompletionPipelineState.java rename to identica-api/src/main/java/me/whereareiam/identica/pipeline/state/completion/CompletionPipelineState.java index 10169facf..5e893a8a3 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/CompletionPipelineState.java +++ b/identica-api/src/main/java/me/whereareiam/identica/pipeline/state/completion/CompletionPipelineState.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.engine.pipeline.completion; +package me.whereareiam.identica.pipeline.state.completion; import lombok.Builder; import lombok.Getter; @@ -9,6 +9,7 @@ import me.whereareiam.identica.model.pipeline.completion.CompletionContext; import me.whereareiam.identica.model.pipeline.completion.CompletionPendingState; import me.whereareiam.identica.model.provider.InternalProvider; +import me.whereareiam.identica.pipeline.state.AbstractGroupState; import me.whereareiam.identica.type.pipeline.PipelineType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -17,12 +18,11 @@ @Setter @ToString @Builder -public class CompletionPipelineState { +public class CompletionPipelineState extends AbstractGroupState { private @NotNull Identity identity; private @NotNull CompletionPendingState pendingState; private @NotNull PipelineType pipelineType; private @Nullable Session session; private @Nullable InternalProvider provider; private @Nullable CompletionContext context; - private boolean authenticationRecognized; } diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/PrepareGroupState.java b/identica-api/src/main/java/me/whereareiam/identica/pipeline/state/prepare/PrepareGroupState.java similarity index 66% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/PrepareGroupState.java rename to identica-api/src/main/java/me/whereareiam/identica/pipeline/state/prepare/PrepareGroupState.java index 558845986..7cf863d86 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/PrepareGroupState.java +++ b/identica-api/src/main/java/me/whereareiam/identica/pipeline/state/prepare/PrepareGroupState.java @@ -1,9 +1,9 @@ -package me.whereareiam.identica.engine.pipeline.prepare.group; +package me.whereareiam.identica.pipeline.state.prepare; import lombok.Getter; import lombok.Setter; -import me.whereareiam.identica.engine.pipeline.scenario.base.AbstractGroupState; import me.whereareiam.identica.model.pipeline.prepare.PrepareRequest; +import me.whereareiam.identica.pipeline.state.AbstractGroupState; import org.jetbrains.annotations.Nullable; @Getter diff --git a/identica-api/src/main/java/me/whereareiam/identica/pipeline/state/scenario/shared/FinalizeState.java b/identica-api/src/main/java/me/whereareiam/identica/pipeline/state/scenario/shared/FinalizeState.java new file mode 100644 index 000000000..51d8a8aea --- /dev/null +++ b/identica-api/src/main/java/me/whereareiam/identica/pipeline/state/scenario/shared/FinalizeState.java @@ -0,0 +1,6 @@ +package me.whereareiam.identica.pipeline.state.scenario.shared; + +import me.whereareiam.identica.pipeline.state.AbstractGroupState; + +public class FinalizeState extends AbstractGroupState { +} diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/journey/JourneyState.java b/identica-api/src/main/java/me/whereareiam/identica/pipeline/state/scenario/shared/JourneyState.java similarity index 80% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/journey/JourneyState.java rename to identica-api/src/main/java/me/whereareiam/identica/pipeline/state/scenario/shared/JourneyState.java index aeab47675..40b738d25 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/journey/JourneyState.java +++ b/identica-api/src/main/java/me/whereareiam/identica/pipeline/state/scenario/shared/JourneyState.java @@ -1,11 +1,11 @@ -package me.whereareiam.identica.engine.pipeline.scenario.base.journey; +package me.whereareiam.identica.pipeline.state.scenario.shared; import lombok.Getter; import lombok.Setter; -import me.whereareiam.identica.engine.pipeline.scenario.base.AbstractGroupState; -import me.whereareiam.identica.pipeline.ScenarioContext; import me.whereareiam.identica.model.pipeline.journey.JourneyStateItem; import me.whereareiam.identica.model.pipeline.journey.execution.JourneyExecutionPlan; +import me.whereareiam.identica.pipeline.ScenarioContext; +import me.whereareiam.identica.pipeline.state.AbstractGroupState; import me.whereareiam.identica.type.pipeline.journey.JourneyMode; import org.jetbrains.annotations.Nullable; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/preparation/PreparationState.java b/identica-api/src/main/java/me/whereareiam/identica/pipeline/state/scenario/shared/PreparationState.java similarity index 63% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/preparation/PreparationState.java rename to identica-api/src/main/java/me/whereareiam/identica/pipeline/state/scenario/shared/PreparationState.java index da51025c4..096dff919 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/preparation/PreparationState.java +++ b/identica-api/src/main/java/me/whereareiam/identica/pipeline/state/scenario/shared/PreparationState.java @@ -1,9 +1,9 @@ -package me.whereareiam.identica.engine.pipeline.scenario.base.preparation; +package me.whereareiam.identica.pipeline.state.scenario.shared; import lombok.Getter; import lombok.Setter; -import me.whereareiam.identica.engine.pipeline.scenario.base.AbstractGroupState; import me.whereareiam.identica.pipeline.ScenarioContext; +import me.whereareiam.identica.pipeline.state.AbstractGroupState; import org.jetbrains.annotations.Nullable; @Getter diff --git a/identica-api/src/main/java/me/whereareiam/identica/pipeline/state/scenario/shared/item/IdentityMetaItem.java b/identica-api/src/main/java/me/whereareiam/identica/pipeline/state/scenario/shared/item/IdentityMetaItem.java new file mode 100644 index 000000000..081c51e3a --- /dev/null +++ b/identica-api/src/main/java/me/whereareiam/identica/pipeline/state/scenario/shared/item/IdentityMetaItem.java @@ -0,0 +1,15 @@ +package me.whereareiam.identica.pipeline.state.scenario.shared.item; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import me.whereareiam.identica.pipeline.state.PipelineStateItem; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class IdentityMetaItem implements PipelineStateItem { + private boolean resumed; +} diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/identity/IdentityState.java b/identica-api/src/main/java/me/whereareiam/identica/pipeline/state/scenario/type/authentication/IdentityState.java similarity index 79% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/identity/IdentityState.java rename to identica-api/src/main/java/me/whereareiam/identica/pipeline/state/scenario/type/authentication/IdentityState.java index 3e181d34e..ec2b689a7 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/identity/IdentityState.java +++ b/identica-api/src/main/java/me/whereareiam/identica/pipeline/state/scenario/type/authentication/IdentityState.java @@ -1,12 +1,12 @@ -package me.whereareiam.identica.engine.pipeline.scenario.authentication.group.identity; +package me.whereareiam.identica.pipeline.state.scenario.type.authentication; import lombok.Getter; import lombok.Setter; -import me.whereareiam.identica.engine.pipeline.scenario.base.AbstractGroupState; import me.whereareiam.identica.model.identity.Account; import me.whereareiam.identica.model.identity.provider.AccountProviderLink; import me.whereareiam.identica.model.identity.provider.AccountProviderProfile; import me.whereareiam.identica.model.provider.ProviderContext; +import me.whereareiam.identica.pipeline.state.AbstractGroupState; import org.jetbrains.annotations.Nullable; @Getter @@ -16,6 +16,5 @@ public class IdentityState extends AbstractGroupState { private @Nullable AccountProviderLink providerLink; private @Nullable AccountProviderProfile profile; private @Nullable String originalUsername; - private @Nullable ProviderContext provider; } diff --git a/identica-api/src/main/java/me/whereareiam/identica/pipeline/state/scenario/type/authentication/PolicyState.java b/identica-api/src/main/java/me/whereareiam/identica/pipeline/state/scenario/type/authentication/PolicyState.java new file mode 100644 index 000000000..eaa500edd --- /dev/null +++ b/identica-api/src/main/java/me/whereareiam/identica/pipeline/state/scenario/type/authentication/PolicyState.java @@ -0,0 +1,10 @@ +package me.whereareiam.identica.pipeline.state.scenario.type.authentication; + +import lombok.Getter; +import lombok.Setter; +import me.whereareiam.identica.pipeline.state.AbstractGroupState; + +@Getter +@Setter +public class PolicyState extends AbstractGroupState { +} diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/session/SessionState.java b/identica-api/src/main/java/me/whereareiam/identica/pipeline/state/scenario/type/authentication/SessionState.java similarity index 67% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/session/SessionState.java rename to identica-api/src/main/java/me/whereareiam/identica/pipeline/state/scenario/type/authentication/SessionState.java index d9ade64c1..5ac23df77 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/session/SessionState.java +++ b/identica-api/src/main/java/me/whereareiam/identica/pipeline/state/scenario/type/authentication/SessionState.java @@ -1,10 +1,10 @@ -package me.whereareiam.identica.engine.pipeline.scenario.authentication.group.session; +package me.whereareiam.identica.pipeline.state.scenario.type.authentication; import lombok.Getter; import lombok.Setter; -import me.whereareiam.identica.engine.pipeline.scenario.base.AbstractGroupState; import me.whereareiam.identica.model.Session; import me.whereareiam.identica.model.auth.AuthContext; +import me.whereareiam.identica.pipeline.state.AbstractGroupState; import org.jetbrains.annotations.Nullable; @Getter diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/migration/group/identity/IdentityState.java b/identica-api/src/main/java/me/whereareiam/identica/pipeline/state/scenario/type/migration/IdentityState.java similarity index 78% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/migration/group/identity/IdentityState.java rename to identica-api/src/main/java/me/whereareiam/identica/pipeline/state/scenario/type/migration/IdentityState.java index 66732a5c3..6b7d25d63 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/migration/group/identity/IdentityState.java +++ b/identica-api/src/main/java/me/whereareiam/identica/pipeline/state/scenario/type/migration/IdentityState.java @@ -1,12 +1,12 @@ -package me.whereareiam.identica.engine.pipeline.scenario.migration.group.identity; +package me.whereareiam.identica.pipeline.state.scenario.type.migration; import lombok.Getter; import lombok.Setter; -import me.whereareiam.identica.engine.pipeline.scenario.base.AbstractGroupState; import me.whereareiam.identica.model.identity.Account; import me.whereareiam.identica.model.identity.provider.AccountProviderLink; import me.whereareiam.identica.model.identity.provider.AccountProviderProfile; import me.whereareiam.identica.model.migration.MigrationContext; +import me.whereareiam.identica.pipeline.state.AbstractGroupState; import org.jetbrains.annotations.Nullable; @Getter diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/migration/group/session/SessionState.java b/identica-api/src/main/java/me/whereareiam/identica/pipeline/state/scenario/type/migration/SessionState.java similarity index 68% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/migration/group/session/SessionState.java rename to identica-api/src/main/java/me/whereareiam/identica/pipeline/state/scenario/type/migration/SessionState.java index ae5d1e55a..603086ce6 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/migration/group/session/SessionState.java +++ b/identica-api/src/main/java/me/whereareiam/identica/pipeline/state/scenario/type/migration/SessionState.java @@ -1,10 +1,10 @@ -package me.whereareiam.identica.engine.pipeline.scenario.migration.group.session; +package me.whereareiam.identica.pipeline.state.scenario.type.migration; import lombok.Getter; import lombok.Setter; -import me.whereareiam.identica.engine.pipeline.scenario.base.AbstractGroupState; import me.whereareiam.identica.model.Session; import me.whereareiam.identica.model.migration.MigrationContext; +import me.whereareiam.identica.pipeline.state.AbstractGroupState; import org.jetbrains.annotations.Nullable; @Getter diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/identity/IdentityState.java b/identica-api/src/main/java/me/whereareiam/identica/pipeline/state/scenario/type/registration/IdentityState.java similarity index 78% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/identity/IdentityState.java rename to identica-api/src/main/java/me/whereareiam/identica/pipeline/state/scenario/type/registration/IdentityState.java index 9813823b0..47891fc02 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/identity/IdentityState.java +++ b/identica-api/src/main/java/me/whereareiam/identica/pipeline/state/scenario/type/registration/IdentityState.java @@ -1,12 +1,12 @@ -package me.whereareiam.identica.engine.pipeline.scenario.registration.group.identity; +package me.whereareiam.identica.pipeline.state.scenario.type.registration; import lombok.Getter; import lombok.Setter; -import me.whereareiam.identica.engine.pipeline.scenario.base.AbstractGroupState; import me.whereareiam.identica.model.identity.Account; import me.whereareiam.identica.model.identity.provider.AccountProviderLink; import me.whereareiam.identica.model.identity.provider.AccountProviderProfile; import me.whereareiam.identica.model.registration.RegistrationContext; +import me.whereareiam.identica.pipeline.state.AbstractGroupState; import org.jetbrains.annotations.Nullable; @Getter diff --git a/identica-api/src/main/java/me/whereareiam/identica/pipeline/state/scenario/type/registration/PolicyState.java b/identica-api/src/main/java/me/whereareiam/identica/pipeline/state/scenario/type/registration/PolicyState.java new file mode 100644 index 000000000..269727696 --- /dev/null +++ b/identica-api/src/main/java/me/whereareiam/identica/pipeline/state/scenario/type/registration/PolicyState.java @@ -0,0 +1,6 @@ +package me.whereareiam.identica.pipeline.state.scenario.type.registration; + +import me.whereareiam.identica.pipeline.state.AbstractGroupState; + +public class PolicyState extends AbstractGroupState { +} diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/session/SessionState.java b/identica-api/src/main/java/me/whereareiam/identica/pipeline/state/scenario/type/registration/SessionState.java similarity index 68% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/session/SessionState.java rename to identica-api/src/main/java/me/whereareiam/identica/pipeline/state/scenario/type/registration/SessionState.java index 9c010876c..0874577d1 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/session/SessionState.java +++ b/identica-api/src/main/java/me/whereareiam/identica/pipeline/state/scenario/type/registration/SessionState.java @@ -1,10 +1,10 @@ -package me.whereareiam.identica.engine.pipeline.scenario.registration.group.session; +package me.whereareiam.identica.pipeline.state.scenario.type.registration; import lombok.Getter; import lombok.Setter; -import me.whereareiam.identica.engine.pipeline.scenario.base.AbstractGroupState; import me.whereareiam.identica.model.Session; import me.whereareiam.identica.model.registration.RegistrationContext; +import me.whereareiam.identica.pipeline.state.AbstractGroupState; import org.jetbrains.annotations.Nullable; @Getter diff --git a/identica-api/src/main/java/me/whereareiam/identica/provider/IdenticaProvider.java b/identica-api/src/main/java/me/whereareiam/identica/provider/IdenticaProvider.java index d5b524ee2..e62ffee53 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/provider/IdenticaProvider.java +++ b/identica-api/src/main/java/me/whereareiam/identica/provider/IdenticaProvider.java @@ -6,6 +6,8 @@ import me.whereareiam.identica.conflict.resolver.ConflictResolver; import me.whereareiam.identica.model.provider.ProviderDescriptor; import me.whereareiam.identica.model.provider.dependency.ProviderLibraries; +import me.whereareiam.identica.provider.capability.bootstrap.ProviderCapabilityBootstrap; +import me.whereareiam.identica.type.provider.ProviderFeature; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -23,7 +25,16 @@ public abstract class IdenticaProvider { protected @Nullable ProviderPlatformExtension platformExtension; /** - * Provides dependency metadata for this eligibility. + * Provides runtime dependency metadata required before capability bootstraps + * can be resolved. + * + *

Libraries may target either the provider-local runtime classloader or + * the shared capability API parent classloader. Capability runtime artifacts + * and their shared APIs should be declared here so the provider loader can + * make them available before calling {@link #declaredCapabilities()}. Libraries + * with {@code loader = "shared"} are installed into the shared capability + * API parent loader, while libraries without an explicit loader target are + * treated as provider-local.

* * @return provider libraries descriptor */ @@ -40,6 +51,27 @@ public abstract class IdenticaProvider { return List.of(); } + /** + * Provides capability bootstraps declared by this provider. + * + *

The returned bootstraps are resolved after {@link #libraries()} has + * been loaded into the appropriate runtime classloaders.

+ * + * @return capability bootstraps + */ + public @NotNull List declaredCapabilities() { + return List.of(); + } + + /** + * Provides built-in features declared by this provider. + * + * @return built-in feature declarations + */ + public @NotNull List declaredFeatures() { + return List.of(); + } + /** * Provides platform-specific extensions that can be selected by the runtime * loader for the active platform. diff --git a/identica-api/src/main/java/me/whereareiam/identica/provider/ProviderManager.java b/identica-api/src/main/java/me/whereareiam/identica/provider/ProviderManager.java index 839da407e..ac4674da1 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/provider/ProviderManager.java +++ b/identica-api/src/main/java/me/whereareiam/identica/provider/ProviderManager.java @@ -2,7 +2,8 @@ import me.whereareiam.identica.model.provider.InternalProvider; import me.whereareiam.identica.provider.resolver.ProviderResolver; -import me.whereareiam.identica.type.provider.ProviderCapability; +import me.whereareiam.identica.type.provider.ProviderFeature; +import me.whereareiam.identica.type.provider.capability.ProviderCapability; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -46,6 +47,22 @@ public interface ProviderManager { */ @Nullable InternalProvider findProvider(@NotNull ProviderCapability... capabilities); + /** + * Finds providers that advertise all requested built-in features. + * + * @param features required features + * @return immutable list of matching providers ordered by priority + */ + @NotNull List findProvidersByFeatures(@NotNull ProviderFeature... features); + + /** + * Finds the highest priority provider that advertises all requested built-in features. + * + * @param features required features + * @return matching provider or {@code null} when none match + */ + @Nullable InternalProvider findProviderByFeatures(@NotNull ProviderFeature... features); + /** * Registers a resolver used during provider loading. */ diff --git a/identica-api/src/main/java/me/whereareiam/identica/provider/capability/ProviderCapabilityCoordinator.java b/identica-api/src/main/java/me/whereareiam/identica/provider/capability/ProviderCapabilityCoordinator.java new file mode 100644 index 000000000..2f0ce9b2d --- /dev/null +++ b/identica-api/src/main/java/me/whereareiam/identica/provider/capability/ProviderCapabilityCoordinator.java @@ -0,0 +1,75 @@ +package me.whereareiam.identica.provider.capability; + +import com.google.inject.Injector; +import com.google.inject.Module; +import me.whereareiam.identica.model.provider.InternalProvider; +import me.whereareiam.identica.model.provider.ProviderDescriptor; +import me.whereareiam.identica.provider.capability.bootstrap.ProviderCapabilityBootstrap; +import me.whereareiam.identica.provider.capability.contribution.ProviderCapabilityContribution; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Set; + +/** + * Validates and installs provider-supplied capabilities during provider load. + */ +public interface ProviderCapabilityCoordinator { + /** + * Resolves provider-supplied capability bootstraps and updates the + * descriptor's declared capability ids. + * + * @param descriptor provider descriptor + * @param bootstraps provider bootstraps + * @return validated bootstraps in provider-declared order + */ + @NotNull List resolveBootstraps( + @NotNull ProviderDescriptor descriptor, + @Nullable List bootstraps + ); + + /** + * Installs shared global capability runtime for the supplied bootstraps. + * + * @param provider provider being loaded + * @param bootstraps validated capability bootstraps + */ + void installGlobalCapabilities( + @NotNull InternalProvider provider, + @NotNull List bootstraps + ); + + /** + * Resolves provider-local modules for the supplied bootstraps. + * + * @param provider provider being loaded + * @param bootstraps validated capability bootstraps + * @return provider-local capability modules + */ + @NotNull List resolveLocalModules( + @NotNull InternalProvider provider, + @NotNull List bootstraps + ); + + /** + * Resolves capability contributions from a provider injector. + * + * @param injector provider injector + * @return resolved contributions + */ + @NotNull Set resolveCapabilityContributions(@NotNull Injector injector); + + /** + * Validates resolved contributions against declared capabilities. + * + * @param descriptor provider descriptor + * @param bootstraps validated capability bootstraps + * @param contributions resolved contributions + */ + void validateCapabilityContributions( + @NotNull ProviderDescriptor descriptor, + @NotNull List bootstraps, + @NotNull Set contributions + ); +} diff --git a/identica-api/src/main/java/me/whereareiam/identica/provider/capability/ProviderCapabilityRegistry.java b/identica-api/src/main/java/me/whereareiam/identica/provider/capability/ProviderCapabilityRegistry.java new file mode 100644 index 000000000..2fd5784a4 --- /dev/null +++ b/identica-api/src/main/java/me/whereareiam/identica/provider/capability/ProviderCapabilityRegistry.java @@ -0,0 +1,44 @@ +package me.whereareiam.identica.provider.capability; + +import me.whereareiam.identica.model.provider.capability.ProviderCapabilityInstallation; +import me.whereareiam.identica.type.provider.capability.ProviderCapability; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/** + * Registry of installed provider capabilities. + */ +public interface ProviderCapabilityRegistry { + /** + * Finds an installed capability runtime. + * + * @param capability capability to resolve + * @return installed runtime or {@code null} + */ + @Nullable ProviderCapabilityInstallation findInstallation(@NotNull ProviderCapability capability); + + /** + * Registers an installed capability runtime. + * + * @param installation installation to register + * @return registered installation + */ + @NotNull ProviderCapabilityInstallation registerInstallation(@NotNull ProviderCapabilityInstallation installation); + + /** + * Removes an installed capability runtime. + * + * @param capability capability to remove + * @return {@code true} when removed + */ + boolean unregisterInstallation(@NotNull ProviderCapability capability); + + /** + * Returns all installed capability runtimes. + * + * @return immutable installations list + */ + @NotNull List installations(); +} diff --git a/identica-api/src/main/java/me/whereareiam/identica/provider/capability/bootstrap/ProviderCapabilityBootstrap.java b/identica-api/src/main/java/me/whereareiam/identica/provider/capability/bootstrap/ProviderCapabilityBootstrap.java new file mode 100644 index 000000000..b47c61f1d --- /dev/null +++ b/identica-api/src/main/java/me/whereareiam/identica/provider/capability/bootstrap/ProviderCapabilityBootstrap.java @@ -0,0 +1,47 @@ +package me.whereareiam.identica.provider.capability.bootstrap; + +import com.google.inject.Module; +import me.whereareiam.identica.model.provider.capability.ProviderCapabilityDeclaration; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +/** + * Provider-supplied bootstrap for a supported capability. + */ +public interface ProviderCapabilityBootstrap { + /** + * Returns the capability declaration exposed by this bootstrap. + * + * @return capability declaration + */ + @NotNull ProviderCapabilityDeclaration declaration(); + + /** + * Initializes capability runtime state after the global injector is created. + * + * @param context initialization context + */ + default void initialize(@NotNull ProviderCapabilityInitializationContext context) { + } + + /** + * Returns modules installed once into a capability-global child injector. + * + * @param context global installation context + * @return modules to install + */ + default @NotNull List globalModules(@NotNull ProviderCapabilityGlobalInstallContext context) { + return List.of(); + } + + /** + * Returns modules installed into a provider child injector. + * + * @param context provider-local installation context + * @return modules to install + */ + default @NotNull List localModules(@NotNull ProviderCapabilityLocalInstallContext context) { + return List.of(); + } +} diff --git a/identica-api/src/main/java/me/whereareiam/identica/provider/capability/bootstrap/ProviderCapabilityGlobalInstallContext.java b/identica-api/src/main/java/me/whereareiam/identica/provider/capability/bootstrap/ProviderCapabilityGlobalInstallContext.java new file mode 100644 index 000000000..f27030e4e --- /dev/null +++ b/identica-api/src/main/java/me/whereareiam/identica/provider/capability/bootstrap/ProviderCapabilityGlobalInstallContext.java @@ -0,0 +1,22 @@ +package me.whereareiam.identica.provider.capability.bootstrap; + +import com.google.inject.Injector; +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; +import me.whereareiam.identica.type.provider.capability.ProviderCapability; +import org.jetbrains.annotations.NotNull; + +import java.nio.file.Path; + +/** + * Context provided when a capability installs global runtime services. + */ +@Getter +@ToString +@Builder +public final class ProviderCapabilityGlobalInstallContext { + private final @NotNull ProviderCapability capability; + private final @NotNull Injector rootInjector; + private final @NotNull Path capabilitiesPath; +} diff --git a/identica-api/src/main/java/me/whereareiam/identica/provider/capability/bootstrap/ProviderCapabilityInitializationContext.java b/identica-api/src/main/java/me/whereareiam/identica/provider/capability/bootstrap/ProviderCapabilityInitializationContext.java new file mode 100644 index 000000000..724a3a7bf --- /dev/null +++ b/identica-api/src/main/java/me/whereareiam/identica/provider/capability/bootstrap/ProviderCapabilityInitializationContext.java @@ -0,0 +1,21 @@ +package me.whereareiam.identica.provider.capability.bootstrap; + +import com.google.inject.Injector; +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; +import me.whereareiam.identica.type.provider.capability.ProviderCapability; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Context provided when a capability initializes runtime state. + */ +@Getter +@ToString +@Builder +public final class ProviderCapabilityInitializationContext { + private final @NotNull ProviderCapability capability; + private final @NotNull Injector rootInjector; + private final @Nullable Injector globalInjector; +} diff --git a/identica-api/src/main/java/me/whereareiam/identica/provider/capability/bootstrap/ProviderCapabilityLocalInstallContext.java b/identica-api/src/main/java/me/whereareiam/identica/provider/capability/bootstrap/ProviderCapabilityLocalInstallContext.java new file mode 100644 index 000000000..ae806ba29 --- /dev/null +++ b/identica-api/src/main/java/me/whereareiam/identica/provider/capability/bootstrap/ProviderCapabilityLocalInstallContext.java @@ -0,0 +1,21 @@ +package me.whereareiam.identica.provider.capability.bootstrap; + +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; +import me.whereareiam.identica.type.provider.capability.ProviderCapability; +import org.jetbrains.annotations.NotNull; + +import java.nio.file.Path; + +/** + * Context provided when a capability installs provider-local runtime modules. + */ +@Getter +@ToString +@Builder +public final class ProviderCapabilityLocalInstallContext { + private final @NotNull ProviderCapability capability; + private final @NotNull String providerId; + private final @NotNull Path workingPath; +} diff --git a/identica-api/src/main/java/me/whereareiam/identica/provider/capability/contribution/ProviderCapabilityContribution.java b/identica-api/src/main/java/me/whereareiam/identica/provider/capability/contribution/ProviderCapabilityContribution.java new file mode 100644 index 000000000..60a23ac45 --- /dev/null +++ b/identica-api/src/main/java/me/whereareiam/identica/provider/capability/contribution/ProviderCapabilityContribution.java @@ -0,0 +1,16 @@ +package me.whereareiam.identica.provider.capability.contribution; + +import me.whereareiam.identica.type.provider.capability.ProviderCapability; +import org.jetbrains.annotations.NotNull; + +/** + * Provider-local contribution exposed for a declared capability. + */ +public interface ProviderCapabilityContribution { + /** + * Returns the capability supported by this contribution. + * + * @return contributed capability + */ + @NotNull ProviderCapability capability(); +} diff --git a/identica-api/src/main/java/me/whereareiam/identica/provider/restriction/ProviderJoinRestrictionService.java b/identica-api/src/main/java/me/whereareiam/identica/provider/restriction/ProviderJoinRestrictionService.java deleted file mode 100644 index 616af018c..000000000 --- a/identica-api/src/main/java/me/whereareiam/identica/provider/restriction/ProviderJoinRestrictionService.java +++ /dev/null @@ -1,72 +0,0 @@ -package me.whereareiam.identica.provider.restriction; - -import me.whereareiam.identica.identity.actor.ConnectionIdentity; -import me.whereareiam.identica.model.provider.restriction.ProviderJoinRestrictionDecision; -import me.whereareiam.identica.model.provider.restriction.ProviderJoinRestrictionStatus; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.List; -import java.util.Optional; - -/** - * Runtime manager and policy service for provider join restrictions. - */ -public interface ProviderJoinRestrictionService { - /** - * Enables runtime restriction for the provider. - * - * @param providerId provider id - * @return resolved status after enabling - */ - @NotNull ProviderJoinRestrictionStatus enable(@Nullable String providerId); - - /** - * Disables runtime restriction for the provider. - * - * @param providerId provider id - * @return resolved status after disabling - */ - @NotNull ProviderJoinRestrictionStatus disable(@Nullable String providerId); - - /** - * Returns status for a single provider. - * - * @param providerId provider id - * @return status or empty when provider is unknown - */ - @NotNull Optional status(@Nullable String providerId); - - /** - * Returns statuses for all configured providers. - * - * @return provider statuses - */ - @NotNull List statuses(); - - /** - * Evaluates the current join restriction using only the provider id. - * - * @param providerId provider id - * @return resolved restriction decision - */ - @NotNull ProviderJoinRestrictionDecision evaluate(@Nullable String providerId); - - /** - * Evaluates the current join restriction for a full provider join attempt. - * - * @param providerId provider id - * @param providerSubject provider subject - * @param providerUsername provider username - * @param ip player ip - * @param origin connection origin - * @return resolved restriction decision - */ - @NotNull ProviderJoinRestrictionDecision evaluate( - @Nullable String providerId, - @Nullable String providerSubject, - @Nullable String providerUsername, - @Nullable String ip, - @Nullable ConnectionIdentity.Origin origin - ); -} diff --git a/identica-api/src/main/java/me/whereareiam/identica/type/UsernameSource.java b/identica-api/src/main/java/me/whereareiam/identica/type/UsernameSource.java deleted file mode 100644 index e9a7dcfc4..000000000 --- a/identica-api/src/main/java/me/whereareiam/identica/type/UsernameSource.java +++ /dev/null @@ -1,36 +0,0 @@ -package me.whereareiam.identica.type; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** - * Source of the currently active username. - */ -@RequiredArgsConstructor -public enum UsernameSource { - PROVIDER("provider"), - MANUAL("manual"), - SYSTEM("system"); - - @Getter - private final @NotNull String id; - - /** - * Resolve a source from its identifier. - * - * @param id source identifier - * @return resolved source or {@link #PROVIDER} as default - */ - public static @NotNull UsernameSource fromId(@Nullable String id) { - if (id == null || id.isBlank()) return PROVIDER; - - String normalized = id.trim().toLowerCase(); - for (UsernameSource source : values()) - if (source.id.equals(normalized)) - return source; - - return PROVIDER; - } -} diff --git a/identica-api/src/main/java/me/whereareiam/identica/type/migration/MigrationResultStatus.java b/identica-api/src/main/java/me/whereareiam/identica/type/migration/MigrationResultStatus.java index fe25abb1e..1bca3c60e 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/type/migration/MigrationResultStatus.java +++ b/identica-api/src/main/java/me/whereareiam/identica/type/migration/MigrationResultStatus.java @@ -9,6 +9,8 @@ public enum MigrationResultStatus { STARTED, ALREADY_PRIMARY, PRIMARY_SET, + TARGET_UNSUPPORTED, + PROVIDER_UNAVAILABLE, PRECHECK_DENIED, FAILED } diff --git a/identica-api/src/main/java/me/whereareiam/identica/type/pipeline/PipelineScope.java b/identica-api/src/main/java/me/whereareiam/identica/type/pipeline/PipelineScope.java deleted file mode 100644 index 80bcfc893..000000000 --- a/identica-api/src/main/java/me/whereareiam/identica/type/pipeline/PipelineScope.java +++ /dev/null @@ -1,7 +0,0 @@ -package me.whereareiam.identica.type.pipeline; - -public enum PipelineScope { - AUTHENTICATION, - REGISTRATION, - MIGRATION -} diff --git a/identica-api/src/main/java/me/whereareiam/identica/type/provider/ProviderCapability.java b/identica-api/src/main/java/me/whereareiam/identica/type/provider/ProviderCapability.java deleted file mode 100644 index e595c1d9c..000000000 --- a/identica-api/src/main/java/me/whereareiam/identica/type/provider/ProviderCapability.java +++ /dev/null @@ -1,70 +0,0 @@ -package me.whereareiam.identica.type.provider; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import org.jetbrains.annotations.Nullable; - -/** - * Well-known provider capability identifiers. - * - *

Capabilities are advertised in {@code eligibility.json} as a list of ids:

- *
{@code
- * "capabilities": ["online", "authoritative_username"]
- * }
- */ -@RequiredArgsConstructor -public enum ProviderCapability { - /** - * Supports online-mode connections. - */ - ONLINE_MODE("online"), - - /** - * Supports offline-mode connections. - */ - OFFLINE_MODE("offline"), - - /** - * Publishes authoritative usernames that should be replicated. - */ - AUTHORITATIVE_USERNAME("authoritative_username"), - - /** - * Supports account migration to this provider. - */ - MIGRATION("migration"), - - /** - * Supports verification flows such as TOTP. - */ - VERIFICATION("verification"); - - @Getter - private final String id; - - /** - * Checks whether the provided id matches this capability. - * - * @param candidate capability id to compare - * @return {@code true} when the id matches - */ - public boolean matches(@Nullable String candidate) { - if (candidate == null || candidate.isBlank()) return false; - return id.equalsIgnoreCase(candidate.trim()); - } - - /** - * Resolves a capability from its id. - * - * @param id capability id - * @return capability or {@code null} when unknown - */ - public static @Nullable ProviderCapability fromId(@Nullable String id) { - if (id == null || id.isBlank()) return null; - for (ProviderCapability capability : values()) - if (capability.matches(id)) - return capability; - - return null; - } -} diff --git a/identica-api/src/main/java/me/whereareiam/identica/type/provider/ProviderFeature.java b/identica-api/src/main/java/me/whereareiam/identica/type/provider/ProviderFeature.java new file mode 100644 index 000000000..2bace8d14 --- /dev/null +++ b/identica-api/src/main/java/me/whereareiam/identica/type/provider/ProviderFeature.java @@ -0,0 +1,49 @@ +package me.whereareiam.identica.type.provider; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Locale; + +/** + * Stable identifier for a built-in provider feature. + */ +@Getter +@ToString +@EqualsAndHashCode +public final class ProviderFeature { + private final @NotNull String id; + + private ProviderFeature(@NotNull String id) { + String normalized = id.trim().toLowerCase(Locale.ROOT); + if (normalized.isBlank()) + throw new IllegalArgumentException("feature id cannot be blank"); + + this.id = normalized; + } + + /** + * Creates a normalized feature identifier. + * + * @param id raw feature id + * @return normalized feature id + */ + public static @NotNull ProviderFeature of(@NotNull String id) { + return new ProviderFeature(id); + } + + /** + * Checks whether the supplied id matches this feature. + * + * @param candidate raw candidate id + * @return {@code true} when the ids match + */ + public boolean matches(@Nullable String candidate) { + if (candidate == null || candidate.isBlank()) return false; + + return id.equals(candidate.trim().toLowerCase(Locale.ROOT)); + } +} diff --git a/identica-api/src/main/java/me/whereareiam/identica/type/provider/ProviderJoinRestrictionCondition.java b/identica-api/src/main/java/me/whereareiam/identica/type/provider/ProviderJoinRestrictionCondition.java deleted file mode 100644 index be637dabc..000000000 --- a/identica-api/src/main/java/me/whereareiam/identica/type/provider/ProviderJoinRestrictionCondition.java +++ /dev/null @@ -1,15 +0,0 @@ -package me.whereareiam.identica.type.provider; - -/** - * Atomic condition that may allow a player through an active provider join restriction. - */ -public enum ProviderJoinRestrictionCondition { - /** - * Allow reconnect-recognized players. - */ - RECOGNIZED, - /** - * Allow players with an existing provider link. - */ - LINKED -} diff --git a/identica-api/src/main/java/me/whereareiam/identica/type/provider/capability/ProviderCapability.java b/identica-api/src/main/java/me/whereareiam/identica/type/provider/capability/ProviderCapability.java new file mode 100644 index 000000000..8eda37b5a --- /dev/null +++ b/identica-api/src/main/java/me/whereareiam/identica/type/provider/capability/ProviderCapability.java @@ -0,0 +1,54 @@ +package me.whereareiam.identica.type.provider.capability; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Locale; + +/** + * Normalized provider capability identifier. + * + *

Capabilities are resolved from provider-supplied bootstraps after the + * provider has declared and loaded its required runtime libraries.

+ */ +@Getter +@ToString +@EqualsAndHashCode +public final class ProviderCapability { + private final @NotNull String id; + + private ProviderCapability(@NotNull String id) { + this.id = normalize(id); + } + + /** + * Creates a capability identifier from a raw id. + * + * @param id raw capability id + * @return normalized capability id + */ + public static @NotNull ProviderCapability of(@NotNull String id) { + return new ProviderCapability(id); + } + + /** + * Checks whether a raw id matches this capability. + * + * @param candidate raw capability id + * @return {@code true} when the id matches + */ + public boolean matches(@Nullable String candidate) { + if (candidate == null || candidate.isBlank()) return false; + return id.equals(normalize(candidate)); + } + + private static @NotNull String normalize(@NotNull String value) { + String normalized = value.trim().toLowerCase(Locale.ROOT); + if (normalized.isBlank()) throw new IllegalArgumentException("Provider capability id cannot be blank"); + + return normalized; + } +} diff --git a/identica-api/src/main/java/me/whereareiam/identica/type/provider/capability/ProviderCapabilityScope.java b/identica-api/src/main/java/me/whereareiam/identica/type/provider/capability/ProviderCapabilityScope.java new file mode 100644 index 000000000..c8811c3e0 --- /dev/null +++ b/identica-api/src/main/java/me/whereareiam/identica/type/provider/capability/ProviderCapabilityScope.java @@ -0,0 +1,16 @@ +package me.whereareiam.identica.type.provider.capability; + +/** + * Runtime scope declared by a provider capability bootstrap. + */ +public enum ProviderCapabilityScope { + /** + * Installs shared runtime services once for the whole Identica runtime. + */ + GLOBAL, + + /** + * Installs provider-local bindings in the provider child injector. + */ + LOCAL +} diff --git a/identica-api/src/test/java/me/whereareiam/identica/model/conflict/ConflictModelsTest.java b/identica-api/src/test/java/me/whereareiam/identica/model/conflict/ConflictModelsTest.java new file mode 100644 index 000000000..fb6b1b0c6 --- /dev/null +++ b/identica-api/src/test/java/me/whereareiam/identica/model/conflict/ConflictModelsTest.java @@ -0,0 +1,51 @@ +package me.whereareiam.identica.model.conflict; + +import me.whereareiam.identica.model.conflict.participant.ConflictParticipant; +import me.whereareiam.identica.model.conflict.participant.ConflictParticipantRole; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +@DisplayName("Conflict Models") +class ConflictModelsTest { + private static final ConflictParticipantRole INCOMING = ConflictParticipantRole.of("incoming"); + private static final ConflictAttributeKey CANDIDATE_USERNAME = ConflictAttributeKey.string("candidateUsername"); + private static final ConflictAttributeKey ACCOUNT_UNIQUE_ID = ConflictAttributeKey.of("accountUniqueId", UUID.class); + + @DisplayName("Context exposes typed attributes and participants") + @Test + void contextExposesTypedAttributesAndParticipants() { + ConflictParticipant incoming = ConflictParticipant.builder() + .role(INCOMING) + .build(); + UUID uniqueId = UUID.randomUUID(); + incoming.putAttribute(ACCOUNT_UNIQUE_ID, uniqueId); + + ConflictContext context = ConflictContext.builder() + .key("username") + .hook("prepare") + .build(); + context.putAttribute(CANDIDATE_USERNAME, "Player"); + context.putParticipant(incoming); + + assertEquals("Player", context.getAttribute(CANDIDATE_USERNAME)); + assertEquals(uniqueId, context.getParticipantAttribute(INCOMING, ACCOUNT_UNIQUE_ID)); + assertNull(context.getParticipantAttribute(ConflictParticipantRole.of("missing"), ACCOUNT_UNIQUE_ID)); + } + + @DisplayName("Resolution stores typed effects") + @Test + void resolutionStoresTypedEffects() { + ConflictResolution resolution = ConflictResolution.allow() + .withEffect("incomingEffectiveUsername", "Player_1") + .withEffect("closeExisting", true); + + assertEquals(ConflictResolution.Decision.ALLOW, resolution.getDecision()); + assertEquals("Player_1", resolution.getEffect("incomingEffectiveUsername", String.class)); + assertEquals(Boolean.TRUE, resolution.getEffect("closeExisting", Boolean.class)); + assertTrue(resolution.hasEffect("incomingEffectiveUsername")); + } +} diff --git a/identica-capability/build.gradle.kts b/identica-capability/build.gradle.kts new file mode 100644 index 000000000..6c8344122 --- /dev/null +++ b/identica-capability/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + id("base") +} diff --git a/identica-capability/capability-authoritative-username/authoritative-username-api/build.gradle.kts b/identica-capability/capability-authoritative-username/authoritative-username-api/build.gradle.kts new file mode 100644 index 000000000..fc68a03fc --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-api/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + id("capability") +} + +toolkitPublish { + artifactId.set("authoritative-username-api") +} diff --git a/identica-capability/capability-authoritative-username/authoritative-username-api/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/UsernameConflictSchema.java b/identica-capability/capability-authoritative-username/authoritative-username-api/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/UsernameConflictSchema.java new file mode 100644 index 000000000..1202b2fc3 --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-api/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/UsernameConflictSchema.java @@ -0,0 +1,34 @@ +package me.whereareiam.identica.provider.capability.authoritative.username; + +import me.whereareiam.identica.model.conflict.ConflictAttributeKey; +import me.whereareiam.identica.model.conflict.participant.ConflictParticipantRole; +import me.whereareiam.identica.type.provider.ProviderOrigin; + +import java.util.UUID; + +public final class UsernameConflictSchema { + public static final ConflictParticipantRole ROLE_INCOMING = ConflictParticipantRole.of("incoming"); + public static final ConflictParticipantRole ROLE_EXISTING = ConflictParticipantRole.of("existing"); + + public static final ConflictAttributeKey ATTRIBUTE_CANDIDATE_USERNAME = + ConflictAttributeKey.string("candidateUsername"); + public static final ConflictAttributeKey ATTRIBUTE_ENTRYPOINT_SOURCE = + ConflictAttributeKey.of("entrypointSource", ProviderOrigin.class); + public static final ConflictAttributeKey ATTRIBUTE_INCOMING_PRIMARY_PROVIDER_ID = + ConflictAttributeKey.string("incomingPrimaryProviderId"); + + public static final ConflictAttributeKey PARTICIPANT_ATTRIBUTE_ACCOUNT_UNIQUE_ID = + ConflictAttributeKey.of("accountUniqueId", UUID.class); + public static final ConflictAttributeKey PARTICIPANT_ATTRIBUTE_USERNAME = + ConflictAttributeKey.string("username"); + public static final ConflictAttributeKey PARTICIPANT_ATTRIBUTE_PROVIDER_ID = + ConflictAttributeKey.string("providerId"); + public static final ConflictAttributeKey PARTICIPANT_ATTRIBUTE_PROVIDER_SUBJECT = + ConflictAttributeKey.string("providerSubject"); + public static final ConflictAttributeKey PARTICIPANT_ATTRIBUTE_PROVIDER_USERNAME = + ConflictAttributeKey.string("providerUsername"); + + public static final String EFFECT_CLOSE_EXISTING = "closeExisting"; + public static final String EFFECT_INCOMING_EFFECTIVE_USERNAME = "incomingEffectiveUsername"; + public static final String EFFECT_EXISTING_EFFECTIVE_USERNAME = "existingEffectiveUsername"; +} diff --git a/identica-capability/capability-authoritative-username/authoritative-username-api/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/database/AccountUsernameHistoryPersistenceService.java b/identica-capability/capability-authoritative-username/authoritative-username-api/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/database/AccountUsernameHistoryPersistenceService.java new file mode 100644 index 000000000..8d7499157 --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-api/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/database/AccountUsernameHistoryPersistenceService.java @@ -0,0 +1,25 @@ +package me.whereareiam.identica.provider.capability.authoritative.username.database; + +import me.whereareiam.identica.provider.capability.authoritative.username.model.account.AccountUsernameHistoryEntry; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** + * Records and removes persisted username-history entries for accounts. + */ +public interface AccountUsernameHistoryPersistenceService { + /** + * Records a username-history entry. + * + * @param entry history entry to append + */ + void record(@NotNull AccountUsernameHistoryEntry entry); + + /** + * Deletes all username-history entries for an account. + * + * @param uniqueId account unique id + */ + void deleteAll(@NotNull UUID uniqueId); +} diff --git a/identica-capability/capability-authoritative-username/authoritative-username-api/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/database/AccountUsernameStatePersistenceService.java b/identica-capability/capability-authoritative-username/authoritative-username-api/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/database/AccountUsernameStatePersistenceService.java new file mode 100644 index 000000000..f4cabdc57 --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-api/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/database/AccountUsernameStatePersistenceService.java @@ -0,0 +1,36 @@ +package me.whereareiam.identica.provider.capability.authoritative.username.database; + +import me.whereareiam.identica.provider.capability.authoritative.username.model.account.AccountUsernameState; +import me.whereareiam.identica.provider.capability.authoritative.username.type.AccountUsernameSource; +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; +import java.util.UUID; + +/** + * Persists and retrieves the current username-control state for accounts. + */ +public interface AccountUsernameStatePersistenceService { + /** + * Finds the current username-control state for an account. + * + * @param uniqueId account unique id + * @return stored state, or empty when none exists + */ + @NotNull Optional find(@NotNull UUID uniqueId); + + /** + * Saves the current username-control state for an account. + * + * @param uniqueId account unique id + * @param source current username source + */ + void save(@NotNull UUID uniqueId, @NotNull AccountUsernameSource source); + + /** + * Deletes any stored username-control state for an account. + * + * @param uniqueId account unique id + */ + void delete(@NotNull UUID uniqueId); +} diff --git a/identica-capability/capability-authoritative-username/authoritative-username-api/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/event/UsernameChangedEvent.java b/identica-capability/capability-authoritative-username/authoritative-username-api/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/event/UsernameChangedEvent.java new file mode 100644 index 000000000..2d0b31ace --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-api/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/event/UsernameChangedEvent.java @@ -0,0 +1,26 @@ +package me.whereareiam.identica.provider.capability.authoritative.username.event; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import me.whereareiam.identica.event.base.Event; +import me.whereareiam.identica.event.base.SynchronousEvent; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.UUID; + +/** + * Event fired when authoritative username persistence stores a username change. + */ +@Getter +@Setter +@ToString +@AllArgsConstructor +public class UsernameChangedEvent implements Event, SynchronousEvent { + private final @NotNull UUID accountUniqueId; + private final @NotNull String oldUsername; + private final @NotNull String newUsername; + private final @Nullable String providerId; +} diff --git a/identica-capability/capability-authoritative-username/authoritative-username-api/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/model/AuthoritativeUsernameMessages.java b/identica-capability/capability-authoritative-username/authoritative-username-api/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/model/AuthoritativeUsernameMessages.java new file mode 100644 index 000000000..3e776741c --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-api/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/model/AuthoritativeUsernameMessages.java @@ -0,0 +1,48 @@ +package me.whereareiam.identica.provider.capability.authoritative.username.model; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import me.whereareiam.configura.ConfigDocument; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +@Getter +@Setter +@ToString +public class AuthoritativeUsernameMessages extends ConfigDocument { + private @NotNull Pipeline pipeline; + + @Getter + @Setter + @ToString + public static class Pipeline { + private @NotNull Prepare prepare; + private @NotNull Identity identity; + private @NotNull Policy policy; + + @Getter + @Setter + @ToString + public static class Prepare { + private @NotNull List failed; + } + + @Getter + @Setter + @ToString + public static class Identity { + private @NotNull List synchronizationFailed; + } + + @Getter + @Setter + @ToString + public static class Policy { + private @NotNull List persistenceFailed; + private @NotNull List conflictDenied; + private @NotNull List entrypointRequired; + } + } +} diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/UsernameHistoryEntry.java b/identica-capability/capability-authoritative-username/authoritative-username-api/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/model/account/AccountUsernameHistoryEntry.java similarity index 50% rename from identica-api/src/main/java/me/whereareiam/identica/model/UsernameHistoryEntry.java rename to identica-capability/capability-authoritative-username/authoritative-username-api/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/model/account/AccountUsernameHistoryEntry.java index 3cd3034e2..7cc6daef9 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/UsernameHistoryEntry.java +++ b/identica-capability/capability-authoritative-username/authoritative-username-api/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/model/account/AccountUsernameHistoryEntry.java @@ -1,18 +1,14 @@ -package me.whereareiam.identica.model; +package me.whereareiam.identica.provider.capability.authoritative.username.model.account; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; +import lombok.*; +import me.whereareiam.identica.provider.capability.authoritative.username.type.AccountUsernameSource; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.UUID; /** - * Represents a persisted username change for an account. + * Append-only history entry describing a persisted username change for an account. */ @Getter @Setter @@ -20,11 +16,11 @@ @NoArgsConstructor @AllArgsConstructor @Builder(toBuilder = true) -public class UsernameHistoryEntry { +public class AccountUsernameHistoryEntry { private @NotNull UUID uniqueId; private @Nullable String providerId; private @NotNull String oldUsername; private @NotNull String newUsername; - private @NotNull String source; + private @NotNull AccountUsernameSource source; private long changedAt; } diff --git a/identica-capability/capability-authoritative-username/authoritative-username-api/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/model/account/AccountUsernameState.java b/identica-capability/capability-authoritative-username/authoritative-username-api/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/model/account/AccountUsernameState.java new file mode 100644 index 000000000..056e3a6cb --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-api/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/model/account/AccountUsernameState.java @@ -0,0 +1,22 @@ +package me.whereareiam.identica.provider.capability.authoritative.username.model.account; + +import lombok.*; +import me.whereareiam.identica.provider.capability.authoritative.username.type.AccountUsernameSource; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** + * Current persisted username control state for a single Identica account. + */ +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Builder(toBuilder = true) +public class AccountUsernameState { + private @NotNull UUID uniqueId; + private @NotNull AccountUsernameSource source; + private long updatedAt; +} diff --git a/identica-capability/capability-authoritative-username/authoritative-username-api/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/model/conflict/UsernameConflictResult.java b/identica-capability/capability-authoritative-username/authoritative-username-api/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/model/conflict/UsernameConflictResult.java new file mode 100644 index 000000000..42a2dd8f1 --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-api/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/model/conflict/UsernameConflictResult.java @@ -0,0 +1,25 @@ +package me.whereareiam.identica.provider.capability.authoritative.username.model.conflict; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@Getter +@RequiredArgsConstructor +public class UsernameConflictResult { + private final boolean denied; + private final @Nullable String denialMessage; + private final @NotNull String effectiveUsername; + + public static @NotNull UsernameConflictResult allowed(@NotNull String effectiveUsername) { + return new UsernameConflictResult(false, null, effectiveUsername); + } + + public static @NotNull UsernameConflictResult denied( + @Nullable String denialMessage, + @NotNull String effectiveUsername + ) { + return new UsernameConflictResult(true, denialMessage, effectiveUsername); + } +} diff --git a/identica-capability/capability-authoritative-username/authoritative-username-api/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/model/conflict/UsernameConflictSubject.java b/identica-capability/capability-authoritative-username/authoritative-username-api/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/model/conflict/UsernameConflictSubject.java new file mode 100644 index 000000000..9ecdafa7f --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-api/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/model/conflict/UsernameConflictSubject.java @@ -0,0 +1,28 @@ +package me.whereareiam.identica.provider.capability.authoritative.username.model.conflict; + +import lombok.Builder; +import lombok.Getter; +import me.whereareiam.identica.conflict.ConflictSubject; +import me.whereareiam.identica.model.identity.Account; +import me.whereareiam.identica.model.identity.provider.AccountProviderLink; +import me.whereareiam.identica.model.identity.provider.AccountProviderProfile; +import me.whereareiam.identica.model.provider.ProviderContext; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@Getter +@Builder(toBuilder = true) +public class UsernameConflictSubject implements ConflictSubject { + @Builder.Default + private final @NotNull String hookId = "prepare"; + private final @Nullable String candidateUsername; + private final @Nullable Account account; + private final @Nullable AccountProviderLink incomingLink; + private final @Nullable AccountProviderProfile incomingProfile; + private final @Nullable ProviderContext providerContext; + + @Override + public @NotNull String hook() { + return hookId; + } +} diff --git a/identica-capability/capability-authoritative-username/authoritative-username-api/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/pipeline/UsernameStateItem.java b/identica-capability/capability-authoritative-username/authoritative-username-api/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/pipeline/UsernameStateItem.java new file mode 100644 index 000000000..91ad12477 --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-api/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/pipeline/UsernameStateItem.java @@ -0,0 +1,22 @@ +package me.whereareiam.identica.provider.capability.authoritative.username.pipeline; + +import lombok.*; +import me.whereareiam.identica.pipeline.state.PipelineStateItem; +import me.whereareiam.identica.provider.capability.authoritative.username.type.AccountUsernameSource; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.UUID; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder(toBuilder = true) +public class UsernameStateItem implements PipelineStateItem { + private @Nullable UUID uniqueId; + private @Nullable String previousUsername; + private @Nullable String providerId; + private @Nullable String providerSubject; + private @NotNull AccountUsernameSource source; +} diff --git a/identica-capability/capability-authoritative-username/authoritative-username-api/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/type/AccountUsernameSource.java b/identica-capability/capability-authoritative-username/authoritative-username-api/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/type/AccountUsernameSource.java new file mode 100644 index 000000000..b02b07370 --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-api/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/type/AccountUsernameSource.java @@ -0,0 +1,35 @@ +package me.whereareiam.identica.provider.capability.authoritative.username.type; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Describes which source currently controls username synchronization for an account. + */ +@RequiredArgsConstructor +public enum AccountUsernameSource { + PROVIDER("provider"), + MANUAL("manual"), + SYSTEM("system"); + + @Getter + private final @NotNull String id; + + /** + * Resolves a persisted source identifier into an enum value. + * + * @param id persisted source identifier + * @return resolved source, or {@link #PROVIDER} when missing or unknown + */ + public static @NotNull AccountUsernameSource fromId(@Nullable String id) { + if (id == null || id.isBlank()) return PROVIDER; + + String normalized = id.trim().toLowerCase(); + for (AccountUsernameSource source : values()) + if (source.id.equals(normalized)) return source; + + return PROVIDER; + } +} diff --git a/identica-capability/capability-authoritative-username/authoritative-username-api/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/type/AuthoritativeUsernameCapability.java b/identica-capability/capability-authoritative-username/authoritative-username-api/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/type/AuthoritativeUsernameCapability.java new file mode 100644 index 000000000..215118ce3 --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-api/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/type/AuthoritativeUsernameCapability.java @@ -0,0 +1,11 @@ +package me.whereareiam.identica.provider.capability.authoritative.username.type; + +import me.whereareiam.identica.type.provider.capability.ProviderCapability; +import org.jetbrains.annotations.NotNull; + +/** + * Built-in authoritative username capability identifier. + */ +public final class AuthoritativeUsernameCapability { + public static final @NotNull ProviderCapability CAPABILITY = ProviderCapability.of("authoritative-username"); +} diff --git a/identica-capability/capability-authoritative-username/authoritative-username-common/build.gradle.kts b/identica-capability/capability-authoritative-username/authoritative-username-common/build.gradle.kts new file mode 100644 index 000000000..782c9c907 --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-common/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + id("capability") +} + +dependencies { + api(projects.capabilityAuthoritativeUsernameApi) +} + +toolkitPublish { + artifactId.set("authoritative-username-common") +} diff --git a/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/AuthoritativeUsernameGlobalModule.java b/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/AuthoritativeUsernameGlobalModule.java new file mode 100644 index 000000000..e9c4b83af --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/AuthoritativeUsernameGlobalModule.java @@ -0,0 +1,55 @@ +package me.whereareiam.identica.provider.capability.authoritative.username; + +import com.google.inject.AbstractModule; +import com.google.inject.Provides; +import com.google.inject.Singleton; +import com.google.inject.name.Named; +import lombok.RequiredArgsConstructor; +import me.whereareiam.identica.provider.capability.authoritative.username.config.provider.AuthoritativeUsernameMessagesProvider; +import me.whereareiam.identica.provider.capability.authoritative.username.conflict.UsernameConflictType; +import me.whereareiam.identica.provider.capability.authoritative.username.conflict.factory.UsernameConflictContextFactory; +import me.whereareiam.identica.provider.capability.authoritative.username.conflict.resolver.UsernameConflictResolver; +import me.whereareiam.identica.provider.capability.authoritative.username.model.AuthoritativeUsernameMessages; +import me.whereareiam.identica.provider.capability.authoritative.username.pipeline.AuthoritativeUsernamePipelineExtension; +import me.whereareiam.identica.provider.capability.authoritative.username.pipeline.prepare.group.policy.phase.ApplyAuthoritativeUsernamePhase; +import me.whereareiam.identica.provider.capability.authoritative.username.pipeline.scenario.type.authentication.group.identity.phase.SynchronizeAuthenticationUsernamePhase; +import me.whereareiam.identica.provider.capability.authoritative.username.pipeline.scenario.type.authentication.group.policy.phase.PersistAuthenticationUsernamePhase; +import me.whereareiam.identica.provider.capability.authoritative.username.pipeline.scenario.type.authentication.group.policy.phase.ReviewAuthenticationUsernamePhase; +import me.whereareiam.identica.provider.capability.authoritative.username.pipeline.scenario.type.migration.group.identity.phase.SynchronizeMigrationUsernamePhase; +import me.whereareiam.identica.provider.capability.authoritative.username.pipeline.scenario.type.registration.group.identity.phase.SynchronizeRegistrationUsernamePhase; +import me.whereareiam.identica.provider.capability.authoritative.username.pipeline.scenario.type.registration.group.policy.phase.PersistRegistrationUsernamePhase; +import me.whereareiam.identica.provider.capability.authoritative.username.pipeline.scenario.type.registration.group.policy.phase.ReviewRegistrationUsernamePhase; + +import java.nio.file.Path; + +@RequiredArgsConstructor +public class AuthoritativeUsernameGlobalModule extends AbstractModule { + private final Path capabilityPath; + + @Override + protected void configure() { + bind(AuthoritativeUsernameMessagesProvider.class).asEagerSingleton(); + bind(AuthoritativeUsernameMessages.class).toProvider(AuthoritativeUsernameMessagesProvider.class); + + bind(UsernameConflictContextFactory.class).asEagerSingleton(); + bind(UsernameConflictResolver.class).asEagerSingleton(); + bind(UsernameConflictType.class).asEagerSingleton(); + + bind(ApplyAuthoritativeUsernamePhase.class).asEagerSingleton(); + bind(SynchronizeAuthenticationUsernamePhase.class).asEagerSingleton(); + bind(SynchronizeRegistrationUsernamePhase.class).asEagerSingleton(); + bind(SynchronizeMigrationUsernamePhase.class).asEagerSingleton(); + bind(ReviewAuthenticationUsernamePhase.class).asEagerSingleton(); + bind(ReviewRegistrationUsernamePhase.class).asEagerSingleton(); + bind(PersistAuthenticationUsernamePhase.class).asEagerSingleton(); + bind(PersistRegistrationUsernamePhase.class).asEagerSingleton(); + bind(AuthoritativeUsernamePipelineExtension.class).asEagerSingleton(); + } + + @Provides + @Singleton + @Named("authoritativeUsernameCapabilityPath") + Path provideCapabilityPath() { + return capabilityPath; + } +} diff --git a/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/config/defaults/AuthoritativeUsernameMessagesDefaults.java b/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/config/defaults/AuthoritativeUsernameMessagesDefaults.java new file mode 100644 index 000000000..aa78d405e --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/config/defaults/AuthoritativeUsernameMessagesDefaults.java @@ -0,0 +1,68 @@ +package me.whereareiam.identica.provider.capability.authoritative.username.config.defaults; + +import com.google.inject.Singleton; +import me.whereareiam.configura.merge.defaults.DefaultsProvider; +import me.whereareiam.identica.provider.capability.authoritative.username.model.AuthoritativeUsernameMessages; + +import java.util.List; + +@Singleton +public class AuthoritativeUsernameMessagesDefaults implements DefaultsProvider { + @Override + public AuthoritativeUsernameMessages supply(AuthoritativeUsernameMessages messages) { + AuthoritativeUsernameMessages.Pipeline pipeline = new AuthoritativeUsernameMessages.Pipeline(); + + AuthoritativeUsernameMessages.Pipeline.Prepare prepare = new AuthoritativeUsernameMessages.Pipeline.Prepare(); + prepare.setFailed(List.of( + "ɪᴅᴇɴᴛɪᴄᴀ", + "", + "Unable to prepare authoritative username.", + "Please contact a server administrator.", + "", + "discord.arcadeya.com" + )); + + AuthoritativeUsernameMessages.Pipeline.Identity identity = new AuthoritativeUsernameMessages.Pipeline.Identity(); + identity.setSynchronizationFailed(List.of( + "ɪᴅᴇɴᴛɪᴄᴀ", + "", + "Unable to synchronize username.", + "Please contact a server administrator.", + "", + "discord.arcadeya.com" + )); + + AuthoritativeUsernameMessages.Pipeline.Policy policy = new AuthoritativeUsernameMessages.Pipeline.Policy(); + policy.setPersistenceFailed(List.of( + "ɪᴅᴇɴᴛɪᴄᴀ", + "", + "Unable to persist username changes.", + "Please contact a server administrator.", + "", + "discord.arcadeya.com" + )); + policy.setConflictDenied(List.of( + "ɪᴅᴇɴᴛɪᴄᴀ", + "", + "That username is currently unavailable.", + "Please contact a server administrator if this looks wrong.", + "", + "discord.arcadeya.com" + )); + policy.setEntrypointRequired(List.of( + "ɪᴅᴇɴᴛɪᴄᴀ", + "", + "This username is already active through another provider.", + "Use the correct entrypoint for {existingProvider} or {incomingProvider}.", + "", + "discord.arcadeya.com" + )); + + pipeline.setPrepare(prepare); + pipeline.setIdentity(identity); + pipeline.setPolicy(policy); + messages.setPipeline(pipeline); + + return messages; + } +} diff --git a/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/config/provider/AuthoritativeUsernameMessagesProvider.java b/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/config/provider/AuthoritativeUsernameMessagesProvider.java new file mode 100644 index 000000000..9cc4df351 --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/config/provider/AuthoritativeUsernameMessagesProvider.java @@ -0,0 +1,33 @@ +package me.whereareiam.identica.provider.capability.authoritative.username.config.provider; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.google.inject.name.Named; +import me.whereareiam.configura.Config; +import me.whereareiam.configura.Configura; +import me.whereareiam.identica.Registry; +import me.whereareiam.identica.Reloadable; +import me.whereareiam.identica.config.ConfigProvider; +import me.whereareiam.identica.provider.capability.authoritative.username.config.defaults.AuthoritativeUsernameMessagesDefaults; +import me.whereareiam.identica.provider.capability.authoritative.username.model.AuthoritativeUsernameMessages; + +import java.nio.file.Path; + +@Singleton +public class AuthoritativeUsernameMessagesProvider extends ConfigProvider { + @Inject + public AuthoritativeUsernameMessagesProvider( + @Named("authoritativeUsernameCapabilityPath") Path capabilityPath, + Registry reloadables + ) { + super(capabilityPath, "messages", AuthoritativeUsernameMessages.class, reloadables); + } + + @Override + protected Configura configura() { + return versioned( + Config.configured().withDefaults(AuthoritativeUsernameMessagesDefaults.class), + AuthoritativeUsernameMessages.class + ); + } +} diff --git a/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/conflict/UsernameConflictType.java b/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/conflict/UsernameConflictType.java new file mode 100644 index 000000000..89244ca7c --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/conflict/UsernameConflictType.java @@ -0,0 +1,79 @@ +package me.whereareiam.identica.provider.capability.authoritative.username.conflict; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import lombok.RequiredArgsConstructor; +import me.whereareiam.identica.conflict.ConflictGuard; +import me.whereareiam.identica.conflict.ConflictType; +import me.whereareiam.identica.conflict.resolver.ConflictResolver; +import me.whereareiam.identica.model.conflict.ConflictContext; +import me.whereareiam.identica.provider.capability.authoritative.username.UsernameConflictSchema; +import me.whereareiam.identica.provider.capability.authoritative.username.conflict.factory.UsernameConflictContextFactory; +import me.whereareiam.identica.provider.capability.authoritative.username.conflict.resolver.UsernameFormatResolver; +import me.whereareiam.identica.provider.capability.authoritative.username.model.conflict.UsernameConflictSubject; +import me.whereareiam.identica.type.ConflictHook; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +@Singleton +@RequiredArgsConstructor(onConstructor_ = @Inject) +public class UsernameConflictType implements ConflictType { + private final UsernameConflictContextFactory contextFactory; + private final UsernameFormatResolver formatResolver; + private final UsernameEntrypointGuard entrypointGuard; + + @Override + public @NotNull String getKey() { + return "username"; + } + + @Override + public @NotNull ConflictHook getDefaultHook() { + return ConflictHook.PREPARE; + } + + @Override + public @NotNull List getResolvers() { + return List.of(formatResolver); + } + + @Override + public @NotNull List getGuards() { + return List.of(entrypointGuard); + } + + @Override + public @Nullable ConflictContext createContext(@NotNull UsernameConflictSubject subject) { + return contextFactory.create(subject); + } + + @Override + public boolean matchesRule(@NotNull ConflictContext context, @NotNull JsonNode when) { + JsonNode providers = when.path("providers"); + if (!providers.isArray() || providers.isEmpty()) return false; + + String incomingProvider = context.getParticipantAttribute( + UsernameConflictSchema.ROLE_INCOMING, + UsernameConflictSchema.PARTICIPANT_ATTRIBUTE_PROVIDER_ID + ); + String existingProvider = context.getParticipantAttribute( + UsernameConflictSchema.ROLE_EXISTING, + UsernameConflictSchema.PARTICIPANT_ATTRIBUTE_PROVIDER_ID + ); + if (incomingProvider == null || existingProvider == null) return false; + + boolean hasIncoming = false; + boolean hasExisting = false; + for (JsonNode providerNode : providers) { + String provider = providerNode != null ? providerNode.asText() : ""; + if (provider.isBlank()) continue; + if (provider.equalsIgnoreCase(incomingProvider)) hasIncoming = true; + if (provider.equalsIgnoreCase(existingProvider)) hasExisting = true; + } + + return hasIncoming && hasExisting; + } +} diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/conflict/guard/UsernameEntrypointConflictGuard.java b/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/conflict/UsernameEntrypointGuard.java similarity index 66% rename from identica-common/src/main/java/me/whereareiam/identica/common/conflict/guard/UsernameEntrypointConflictGuard.java rename to identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/conflict/UsernameEntrypointGuard.java index 7e15cae30..8117dc26d 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/conflict/guard/UsernameEntrypointConflictGuard.java +++ b/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/conflict/UsernameEntrypointGuard.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.common.conflict.guard; +package me.whereareiam.identica.provider.capability.authoritative.username.conflict; import com.google.inject.Inject; import com.google.inject.Provider; @@ -6,11 +6,11 @@ import lombok.RequiredArgsConstructor; import me.whereareiam.identica.conflict.ConflictGuard; import me.whereareiam.identica.logging.Logger; -import me.whereareiam.identica.model.config.Messages; import me.whereareiam.identica.model.conflict.ConflictContext; import me.whereareiam.identica.model.conflict.ConflictResolution; -import me.whereareiam.identica.model.identity.provider.AccountProviderLink; import me.whereareiam.identica.provider.ProviderOperations; +import me.whereareiam.identica.provider.capability.authoritative.username.UsernameConflictSchema; +import me.whereareiam.identica.provider.capability.authoritative.username.model.AuthoritativeUsernameMessages; import me.whereareiam.identica.type.provider.ProviderOrigin; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -21,24 +21,28 @@ @Singleton @RequiredArgsConstructor(onConstructor_ = @Inject) -public class UsernameEntrypointConflictGuard implements ConflictGuard { - private final Provider messagesProvider; +public class UsernameEntrypointGuard implements ConflictGuard { + private final Provider messagesProvider; private final ProviderOperations providerOperations; @Override public @Nullable ConflictResolution guard(@NotNull ConflictContext context) { - if (!"username".equalsIgnoreCase(context.getKey())) - return null; - - AccountProviderLink incoming = context.getIncomingLink(); - AccountProviderLink existing = context.getExistingLink(); - if (incoming == null || existing == null) return null; + if (!"username".equalsIgnoreCase(context.getKey())) return null; - String incomingProvider = incoming.getProviderId(); - String existingProvider = existing.getProviderId(); + String incomingProvider = context.getParticipantAttribute( + UsernameConflictSchema.ROLE_INCOMING, + UsernameConflictSchema.PARTICIPANT_ATTRIBUTE_PROVIDER_ID + ); + String existingProvider = context.getParticipantAttribute( + UsernameConflictSchema.ROLE_EXISTING, + UsernameConflictSchema.PARTICIPANT_ATTRIBUTE_PROVIDER_ID + ); + if (incomingProvider == null || existingProvider == null) return null; if (incomingProvider.equalsIgnoreCase(existingProvider)) return null; - ProviderOrigin source = context.getExtra("entrypointSource", ProviderOrigin.class); + ProviderOrigin source = context.getAttribute( + UsernameConflictSchema.ATTRIBUTE_ENTRYPOINT_SOURCE + ); if (source == ProviderOrigin.ENTRYPOINT) { Logger.debug("Username conflict accepted by entrypoint incoming=%s existing=%s", incomingProvider, @@ -53,18 +57,14 @@ public class UsernameEntrypointConflictGuard implements ConflictGuard { Logger.debug("Username conflict requires entrypoint incoming=%s existing=%s", incomingProvider, existingProvider); - String message = resolveEntrypointMessage(incomingProvider, existingProvider); - return ConflictResolution.deny(message); + return ConflictResolution.deny(resolveEntrypointMessage(incomingProvider, existingProvider)); } private @Nullable String resolveEntrypointMessage( @Nullable String incomingProvider, @Nullable String existingProvider ) { - List lines = messagesProvider.get() - .getScenarios() - .getAuthentication() - .getConflictEntrypointRequired(); + List lines = messagesProvider.get().getPipeline().getPolicy().getEntrypointRequired(); if (lines.isEmpty()) return null; String message = String.join("\n", lines); @@ -78,8 +78,9 @@ public class UsernameEntrypointConflictGuard implements ConflictGuard { ); String resolved = message; - for (Map.Entry entry : placeholders.entrySet()) + for (Map.Entry entry : placeholders.entrySet()) { resolved = resolved.replace("{" + entry.getKey() + "}", entry.getValue()); + } return resolved; } diff --git a/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/conflict/factory/UsernameConflictContextFactory.java b/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/conflict/factory/UsernameConflictContextFactory.java new file mode 100644 index 000000000..093be5866 --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/conflict/factory/UsernameConflictContextFactory.java @@ -0,0 +1,146 @@ +package me.whereareiam.identica.provider.capability.authoritative.username.conflict.factory; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import lombok.RequiredArgsConstructor; +import me.whereareiam.identica.database.AccountPersistenceService; +import me.whereareiam.identica.database.provider.ProviderLinkPersistenceService; +import me.whereareiam.identica.database.provider.ProviderProfilePersistenceService; +import me.whereareiam.identica.identity.session.SessionService; +import me.whereareiam.identica.model.conflict.ConflictContext; +import me.whereareiam.identica.model.conflict.participant.ConflictParticipant; +import me.whereareiam.identica.model.identity.Account; +import me.whereareiam.identica.model.identity.provider.AccountProviderLink; +import me.whereareiam.identica.model.identity.provider.AccountProviderProfile; +import me.whereareiam.identica.model.provider.ProviderContext; +import me.whereareiam.identica.provider.capability.authoritative.username.UsernameConflictSchema; +import me.whereareiam.identica.provider.capability.authoritative.username.model.conflict.UsernameConflictSubject; +import me.whereareiam.identica.type.provider.ProviderOrigin; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Singleton +@RequiredArgsConstructor(onConstructor_ = @Inject) +public class UsernameConflictContextFactory { + private final AccountPersistenceService accountPersistenceService; + private final ProviderLinkPersistenceService providerLinkPersistenceService; + private final ProviderProfilePersistenceService providerProfilePersistenceService; + private final SessionService sessionService; + + public @Nullable ConflictContext create(@NotNull UsernameConflictSubject subject) { + String candidate = subject.getCandidateUsername(); + if (candidate == null || candidate.isBlank()) return null; + + Account account = subject.getAccount(); + AccountProviderLink incomingLink = subject.getIncomingLink(); + if (account == null || incomingLink == null) return null; + + Account conflictAccount = resolveActiveConflict(account.getUniqueId(), candidate.trim()).orElse(null); + if (conflictAccount == null) return null; + + AccountProviderLink existingLink = resolvePrimaryLink(conflictAccount.getUniqueId()).orElse(null); + if (existingLink == null) return null; + + AccountProviderProfile existingProfile = providerProfilePersistenceService.findBySubject( + existingLink.getProviderId(), + existingLink.getProviderSubject() + ).orElse(null); + + ConflictContext context = ConflictContext.builder() + .key("username") + .hook(subject.hook()) + .build(); + context.putAttribute(UsernameConflictSchema.ATTRIBUTE_CANDIDATE_USERNAME, candidate.trim()); + context.putParticipant(buildIncomingParticipant(account, incomingLink, subject.getIncomingProfile())); + context.putParticipant(buildExistingParticipant(conflictAccount, existingLink, existingProfile)); + + ProviderContext providerContext = subject.getProviderContext(); + ProviderOrigin source = providerContext != null ? providerContext.getSource() : null; + if (source != null) + context.putAttribute(UsernameConflictSchema.ATTRIBUTE_ENTRYPOINT_SOURCE, source); + + resolvePrimaryLink(account.getUniqueId()).ifPresent(primaryIncoming -> + context.putAttribute( + UsernameConflictSchema.ATTRIBUTE_INCOMING_PRIMARY_PROVIDER_ID, + primaryIncoming.getProviderId() + ) + ); + return context; + } + + private @NotNull ConflictParticipant buildIncomingParticipant( + @NotNull Account account, + @NotNull AccountProviderLink link, + @Nullable AccountProviderProfile profile + ) { + ConflictParticipant participant = ConflictParticipant.builder() + .role(UsernameConflictSchema.ROLE_INCOMING) + .build(); + participant.putAttribute(UsernameConflictSchema.PARTICIPANT_ATTRIBUTE_ACCOUNT_UNIQUE_ID, account.getUniqueId()); + participant.putAttribute(UsernameConflictSchema.PARTICIPANT_ATTRIBUTE_USERNAME, account.getUsername()); + participant.putAttribute(UsernameConflictSchema.PARTICIPANT_ATTRIBUTE_PROVIDER_ID, link.getProviderId()); + participant.putAttribute(UsernameConflictSchema.PARTICIPANT_ATTRIBUTE_PROVIDER_SUBJECT, link.getProviderSubject()); + if (profile != null) + participant.putAttribute( + UsernameConflictSchema.PARTICIPANT_ATTRIBUTE_PROVIDER_USERNAME, + profile.getProviderUsername() + ); + + return participant; + } + + private @NotNull ConflictParticipant buildExistingParticipant( + @NotNull Account account, + @NotNull AccountProviderLink link, + @Nullable AccountProviderProfile profile + ) { + ConflictParticipant participant = ConflictParticipant.builder() + .role(UsernameConflictSchema.ROLE_EXISTING) + .build(); + participant.putAttribute(UsernameConflictSchema.PARTICIPANT_ATTRIBUTE_ACCOUNT_UNIQUE_ID, account.getUniqueId()); + participant.putAttribute(UsernameConflictSchema.PARTICIPANT_ATTRIBUTE_USERNAME, account.getUsername()); + participant.putAttribute(UsernameConflictSchema.PARTICIPANT_ATTRIBUTE_PROVIDER_ID, link.getProviderId()); + participant.putAttribute(UsernameConflictSchema.PARTICIPANT_ATTRIBUTE_PROVIDER_SUBJECT, link.getProviderSubject()); + if (profile != null) + participant.putAttribute( + UsernameConflictSchema.PARTICIPANT_ATTRIBUTE_PROVIDER_USERNAME, + profile.getProviderUsername() + ); + + return participant; + } + + private @NotNull Optional resolveActiveConflict(@NotNull UUID uniqueId, @NotNull String username) { + List accounts = accountPersistenceService.findByUsername(username); + if (accounts.isEmpty()) return Optional.empty(); + + for (Account account : accounts) { + if (account == null) continue; + if (account.getUniqueId().equals(uniqueId)) continue; + if (isActive(account.getUniqueId())) return Optional.of(account); + } + + return Optional.empty(); + } + + private @NotNull Optional resolvePrimaryLink(@NotNull UUID uniqueId) { + List links = providerLinkPersistenceService.findByUniqueId(uniqueId); + if (links.isEmpty()) return Optional.empty(); + + for (AccountProviderLink link : links) { + if (link != null && link.isPrimaryLink()) return Optional.of(link); + } + + return Optional.ofNullable(links.getFirst()); + } + + private boolean isActive(@NotNull UUID uniqueId) { + return sessionService.findByUniqueId(uniqueId) + .thenApply(Optional::isPresent) + .join(); + } +} diff --git a/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/conflict/resolver/UsernameConflictResolver.java b/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/conflict/resolver/UsernameConflictResolver.java new file mode 100644 index 000000000..188b7b695 --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/conflict/resolver/UsernameConflictResolver.java @@ -0,0 +1,96 @@ +package me.whereareiam.identica.provider.capability.authoritative.username.conflict.resolver; + +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import lombok.RequiredArgsConstructor; +import me.whereareiam.identica.conflict.ConflictService; +import me.whereareiam.identica.identity.session.SessionService; +import me.whereareiam.identica.model.Session; +import me.whereareiam.identica.model.conflict.ConflictContext; +import me.whereareiam.identica.model.conflict.ConflictResolution; +import me.whereareiam.identica.provider.capability.authoritative.username.UsernameConflictSchema; +import me.whereareiam.identica.provider.capability.authoritative.username.conflict.UsernameConflictType; +import me.whereareiam.identica.provider.capability.authoritative.username.model.AuthoritativeUsernameMessages; +import me.whereareiam.identica.provider.capability.authoritative.username.model.conflict.UsernameConflictResult; +import me.whereareiam.identica.provider.capability.authoritative.username.model.conflict.UsernameConflictSubject; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +@Singleton +@RequiredArgsConstructor(onConstructor_ = @Inject) +public class UsernameConflictResolver { + private final ConflictService conflictService; + private final UsernameConflictType conflictType; + private final SessionService sessionService; + private final Provider messagesProvider; + + public @NotNull UsernameConflictResult handle( + @NotNull UsernameConflictSubject subject, + @NotNull String currentEffectiveUsername + ) { + ConflictContext context = conflictType.createContext(subject); + if (context == null) return UsernameConflictResult.allowed(currentEffectiveUsername); + + ConflictResolution resolution = conflictService.resolve(context); + if (resolution == null) return UsernameConflictResult.allowed(currentEffectiveUsername); + + String effectiveUsername = currentEffectiveUsername; + if (Boolean.TRUE.equals(resolution.getEffect(UsernameConflictSchema.EFFECT_CLOSE_EXISTING, Boolean.class))) + closeExisting(context); + + String existingEffectiveUsername = resolution.getEffect( + UsernameConflictSchema.EFFECT_EXISTING_EFFECTIVE_USERNAME, + String.class + ); + if (existingEffectiveUsername != null && !existingEffectiveUsername.isBlank()) + applyExistingEffectiveUsername(context, existingEffectiveUsername); + + String incomingEffectiveUsername = resolution.getEffect( + UsernameConflictSchema.EFFECT_INCOMING_EFFECTIVE_USERNAME, + String.class + ); + if (incomingEffectiveUsername != null && !incomingEffectiveUsername.isBlank()) + effectiveUsername = incomingEffectiveUsername; + + if (resolution.getDecision() == ConflictResolution.Decision.DENY) + return UsernameConflictResult.denied(resolveMessage(resolution.getMessage()), effectiveUsername); + + return UsernameConflictResult.allowed(effectiveUsername); + } + + private @NotNull String resolveMessage(@Nullable String message) { + if (message != null && !message.isBlank()) return message; + return String.join("\n", messagesProvider.get().getPipeline().getPolicy().getConflictDenied()); + } + + private void closeExisting(@NotNull ConflictContext context) { + UUID uniqueId = context.getParticipantAttribute( + UsernameConflictSchema.ROLE_EXISTING, + UsernameConflictSchema.PARTICIPANT_ATTRIBUTE_ACCOUNT_UNIQUE_ID + ); + if (uniqueId == null) return; + + sessionService.close(uniqueId).join(); + } + + private void applyExistingEffectiveUsername(@NotNull ConflictContext context, @NotNull String effectiveUsername) { + UUID uniqueId = context.getParticipantAttribute( + UsernameConflictSchema.ROLE_EXISTING, + UsernameConflictSchema.PARTICIPANT_ATTRIBUTE_ACCOUNT_UNIQUE_ID + ); + if (uniqueId == null) return; + + sessionService.findByUniqueId(uniqueId) + .thenCompose(found -> { + if (found.isEmpty()) return CompletableFuture.completedFuture(null); + + Session session = found.get(); + session.setEffectiveUsername(effectiveUsername); + return sessionService.open(session).thenApply(ignored -> null); + }).join(); + } +} diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/conflict/resolver/username/FormatUsernameConflictResolver.java b/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/conflict/resolver/UsernameFormatResolver.java similarity index 71% rename from identica-common/src/main/java/me/whereareiam/identica/common/conflict/resolver/username/FormatUsernameConflictResolver.java rename to identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/conflict/resolver/UsernameFormatResolver.java index 295342638..bfc726c7e 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/conflict/resolver/username/FormatUsernameConflictResolver.java +++ b/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/conflict/resolver/UsernameFormatResolver.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.common.conflict.resolver.username; +package me.whereareiam.identica.provider.capability.authoritative.username.conflict.resolver; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -9,21 +9,18 @@ import me.whereareiam.identica.model.conflict.ConflictContext; import me.whereareiam.identica.model.conflict.ConflictResolution; import me.whereareiam.identica.provider.ProviderOperations; +import me.whereareiam.identica.provider.capability.authoritative.username.UsernameConflictSchema; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.Arrays; -import java.util.LinkedHashMap; -import java.util.Locale; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.concurrent.ThreadLocalRandom; import java.util.regex.Matcher; import java.util.regex.Pattern; @Singleton @RequiredArgsConstructor(onConstructor_ = @Inject) -public class FormatUsernameConflictResolver implements TypedConflictResolver { +public class UsernameFormatResolver implements TypedConflictResolver { private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\{([a-zA-Z][a-zA-Z0-9]*(?::[a-zA-Z0-9]+)*)}"); private static final Set PROVIDER_NAME_PLACEHOLDERS = Set.of("incomingProvider", "existingProvider"); private final ProviderOperations providerOperations; @@ -44,25 +41,22 @@ public boolean supports(@NotNull String key) { } @Override - public @NotNull ConflictResolution resolve( - @NotNull ConflictContext context, - @NotNull Config config - ) { + public @NotNull ConflictResolution resolve(@NotNull ConflictContext context, @NotNull Config config) { Config.Format formatConfig = config.getFormat(); String format = formatConfig.getPattern(); - if (format.isBlank()) - return ConflictResolution.allow(); - - String requested = context.getCandidate(); - if (requested.isBlank()) - return ConflictResolution.allow(); - - String incomingProvider = context.getIncomingLink() != null - ? context.getIncomingLink().getProviderId() - : null; - String existingProvider = context.getExistingLink() != null - ? context.getExistingLink().getProviderId() - : null; + if (format.isBlank()) return ConflictResolution.allow(); + + String requested = context.getAttribute(UsernameConflictSchema.ATTRIBUTE_CANDIDATE_USERNAME); + if (requested == null || requested.isBlank()) return ConflictResolution.allow(); + + String incomingProvider = context.getParticipantAttribute( + UsernameConflictSchema.ROLE_INCOMING, + UsernameConflictSchema.PARTICIPANT_ATTRIBUTE_PROVIDER_ID + ); + String existingProvider = context.getParticipantAttribute( + UsernameConflictSchema.ROLE_EXISTING, + UsernameConflictSchema.PARTICIPANT_ATTRIBUTE_PROVIDER_ID + ); String incomingProviderName = providerOperations.displayProviderName(incomingProvider); String existingProviderName = providerOperations.displayProviderName(existingProvider); @@ -73,24 +67,27 @@ public boolean supports(@NotNull String key) { incomingProviderName, existingProviderName )); - if (formatConfig.isUppercase()) - display = display.toUpperCase(Locale.ROOT); - if (formatConfig.isLowercase()) - display = display.toLowerCase(Locale.ROOT); + if (formatConfig.isUppercase()) display = display.toUpperCase(Locale.ROOT); + if (formatConfig.isLowercase()) display = display.toLowerCase(Locale.ROOT); + + Target target = resolveTarget(config.getTarget()); + ConflictResolution resolution = ConflictResolution.allow(); + if (target == Target.INCOMING || target == Target.BOTH) + resolution = resolution.withEffect(UsernameConflictSchema.EFFECT_INCOMING_EFFECTIVE_USERNAME, display); + if (target == Target.EXISTING || target == Target.BOTH) + resolution = resolution.withEffect(UsernameConflictSchema.EFFECT_EXISTING_EFFECTIVE_USERNAME, display); - ConflictResolution.OverrideTarget target = resolveTarget(config.getTarget()); - return ConflictResolution.allowWithOverride(display, target); + return resolution; } - private ConflictResolution.OverrideTarget resolveTarget(@Nullable String raw) { - if (raw == null || raw.isBlank()) - return ConflictResolution.OverrideTarget.INCOMING; + private @NotNull Target resolveTarget(@Nullable String raw) { + if (raw == null || raw.isBlank()) return Target.INCOMING; String normalized = raw.trim().toLowerCase(Locale.ROOT); return switch (normalized) { - case "existing", "playing", "active" -> ConflictResolution.OverrideTarget.EXISTING; - case "both", "all" -> ConflictResolution.OverrideTarget.BOTH; - default -> ConflictResolution.OverrideTarget.INCOMING; + case "existing", "playing", "active" -> Target.EXISTING; + case "both", "all" -> Target.BOTH; + default -> Target.INCOMING; }; } @@ -120,26 +117,20 @@ private String replacePlaceholders(@Nullable String input, @NotNull Map replacements - ) { + private @Nullable String resolvePlaceholder(@NotNull String token, @NotNull Map replacements) { String[] parts = token.split(":"); String name = parts[0]; if ("random".equals(name)) return randomDigits(resolveRandomDigits(parts)); String replacement = replacements.get(name); if (replacement == null) return null; - if (!PROVIDER_NAME_PLACEHOLDERS.contains(name)) return replacement; return truncate(replacement, parseMaxSymbolCount(parts)); @@ -156,7 +147,6 @@ private int resolveRandomDigits(@NotNull String[] parts) { int parsed = Integer.parseInt(modifier); if (parsed < 1) return 1; - return Math.min(parsed, 10); } @@ -165,11 +155,9 @@ private int resolveRandomDigits(@NotNull String[] parts) { private boolean isDigits(@Nullable String value) { if (value == null || value.isBlank()) return false; - for (int i = 0; i < value.length(); i++) { if (!Character.isDigit(value.charAt(i))) return false; } - return true; } @@ -192,6 +180,12 @@ private String randomDigits(int digits) { return out.toString(); } + private enum Target { + INCOMING, + EXISTING, + BOTH + } + @Getter @Setter public static class Config { diff --git a/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/pipeline/AuthoritativeUsernamePipelineExtension.java b/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/pipeline/AuthoritativeUsernamePipelineExtension.java new file mode 100644 index 000000000..5a497006b --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/pipeline/AuthoritativeUsernamePipelineExtension.java @@ -0,0 +1,81 @@ +package me.whereareiam.identica.provider.capability.authoritative.username.pipeline; + +import com.google.inject.Inject; +import lombok.RequiredArgsConstructor; +import me.whereareiam.identica.model.pipeline.phase.PhasePlacement; +import me.whereareiam.identica.pipeline.extension.PipelineExtension; +import me.whereareiam.identica.pipeline.extension.PipelineExtensionBuilder; +import me.whereareiam.identica.provider.capability.authoritative.username.pipeline.prepare.group.policy.phase.ApplyAuthoritativeUsernamePhase; +import me.whereareiam.identica.provider.capability.authoritative.username.pipeline.scenario.type.authentication.group.identity.phase.SynchronizeAuthenticationUsernamePhase; +import me.whereareiam.identica.provider.capability.authoritative.username.pipeline.scenario.type.authentication.group.policy.phase.PersistAuthenticationUsernamePhase; +import me.whereareiam.identica.provider.capability.authoritative.username.pipeline.scenario.type.authentication.group.policy.phase.ReviewAuthenticationUsernamePhase; +import me.whereareiam.identica.provider.capability.authoritative.username.pipeline.scenario.type.migration.group.identity.phase.SynchronizeMigrationUsernamePhase; +import me.whereareiam.identica.provider.capability.authoritative.username.pipeline.scenario.type.registration.group.identity.phase.SynchronizeRegistrationUsernamePhase; +import me.whereareiam.identica.provider.capability.authoritative.username.pipeline.scenario.type.registration.group.policy.phase.PersistRegistrationUsernamePhase; +import me.whereareiam.identica.provider.capability.authoritative.username.pipeline.scenario.type.registration.group.policy.phase.ReviewRegistrationUsernamePhase; +import me.whereareiam.identica.type.pipeline.PipelineType; +import org.jetbrains.annotations.NotNull; + +@RequiredArgsConstructor(onConstructor_ = @Inject) +public class AuthoritativeUsernamePipelineExtension implements PipelineExtension { + private final ApplyAuthoritativeUsernamePhase applyAuthoritativeUsernamePhase; + private final SynchronizeAuthenticationUsernamePhase synchronizeAuthenticationUsernamePhase; + private final SynchronizeRegistrationUsernamePhase synchronizeRegistrationUsernamePhase; + private final SynchronizeMigrationUsernamePhase synchronizeMigrationUsernamePhase; + private final ReviewAuthenticationUsernamePhase reviewAuthenticationUsernamePhase; + private final ReviewRegistrationUsernamePhase reviewRegistrationUsernamePhase; + private final PersistAuthenticationUsernamePhase persistAuthenticationUsernamePhase; + private final PersistRegistrationUsernamePhase persistRegistrationUsernamePhase; + + @Override + public @NotNull String id() { + return "authoritative-username:phases"; + } + + @Override + public void apply(@NotNull PipelineExtensionBuilder builder) { + builder.registerPreparePhase("policy", applyAuthoritativeUsernamePhase, PhasePlacement.first()); + builder.registerScenarioPhase( + PipelineType.AUTHENTICATION, + "identity", + synchronizeAuthenticationUsernamePhase, + PhasePlacement.after("refresh-provider-profile") + ); + builder.registerScenarioPhase( + PipelineType.REGISTRATION, + "identity", + synchronizeRegistrationUsernamePhase, + PhasePlacement.after("link-provider") + ); + builder.registerScenarioPhase( + PipelineType.MIGRATION, + "identity", + synchronizeMigrationUsernamePhase, + PhasePlacement.after("apply-provider-link") + ); + builder.registerScenarioPhase( + PipelineType.AUTHENTICATION, + "policy", + reviewAuthenticationUsernamePhase, + PhasePlacement.first() + ); + builder.registerScenarioPhase( + PipelineType.AUTHENTICATION, + "policy", + persistAuthenticationUsernamePhase, + PhasePlacement.last() + ); + builder.registerScenarioPhase( + PipelineType.REGISTRATION, + "policy", + reviewRegistrationUsernamePhase, + PhasePlacement.first() + ); + builder.registerScenarioPhase( + PipelineType.REGISTRATION, + "policy", + persistRegistrationUsernamePhase, + PhasePlacement.last() + ); + } +} diff --git a/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/pipeline/phase/base/AbstractPersistUsernamePhase.java b/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/pipeline/phase/base/AbstractPersistUsernamePhase.java new file mode 100644 index 000000000..ed560d5eb --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/pipeline/phase/base/AbstractPersistUsernamePhase.java @@ -0,0 +1,82 @@ +package me.whereareiam.identica.provider.capability.authoritative.username.pipeline.phase.base; + +import me.whereareiam.identica.database.AccountPersistenceService; +import me.whereareiam.identica.event.EventManager; +import me.whereareiam.identica.model.pipeline.PipelineResult; +import me.whereareiam.identica.model.pipeline.phase.PhaseResult; +import me.whereareiam.identica.pipeline.PipelinePhase; +import me.whereareiam.identica.pipeline.ScenarioContext; +import me.whereareiam.identica.pipeline.state.AbstractGroupState; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.provider.ProviderManager; +import me.whereareiam.identica.provider.capability.authoritative.username.database.AccountUsernameHistoryPersistenceService; +import me.whereareiam.identica.provider.capability.authoritative.username.database.AccountUsernameStatePersistenceService; +import me.whereareiam.identica.provider.capability.authoritative.username.event.UsernameChangedEvent; +import me.whereareiam.identica.provider.capability.authoritative.username.model.account.AccountUsernameHistoryEntry; +import me.whereareiam.identica.provider.capability.authoritative.username.pipeline.UsernameStateItem; +import me.whereareiam.identica.provider.capability.authoritative.username.type.AccountUsernameSource; +import me.whereareiam.identica.type.pipeline.PipelineStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +public abstract class AbstractPersistUsernamePhase extends AbstractUsernamePhase implements PipelinePhase { + protected final AccountPersistenceService accountPersistenceService; + protected final AccountUsernameHistoryPersistenceService historyPersistenceService; + protected final EventManager eventManager; + + protected AbstractPersistUsernamePhase( + @NotNull ProviderManager providerManager, + @NotNull AccountUsernameStatePersistenceService statePersistenceService, + @NotNull AccountPersistenceService accountPersistenceService, + @NotNull AccountUsernameHistoryPersistenceService historyPersistenceService, + @NotNull EventManager eventManager + ) { + super(providerManager, statePersistenceService); + this.accountPersistenceService = accountPersistenceService; + this.historyPersistenceService = historyPersistenceService; + this.eventManager = eventManager; + } + + @Override + public final @NotNull CompletionStage> execute(@NotNull PipelineState pipelineState, @NotNull S state) { + PipelineResult result = state.getResult(); + if (result == null || result.getStatus() != PipelineStatus.COMPLETE) + return CompletableFuture.completedFuture(PhaseResult.pass(state)); + + UsernameStateItem snapshot = pipelineState.item(UsernameStateItem.class).orElse(null); + ScenarioContext context = scenarioContext(pipelineState, state); + if (snapshot == null || context == null || snapshot.getUniqueId() == null) + return CompletableFuture.completedFuture(PhaseResult.pass(state)); + + String currentUsername = context.getUsername(); + saveSource(snapshot.getUniqueId(), Objects.equals(snapshot.getPreviousUsername(), currentUsername) + ? snapshot.getSource() + : AccountUsernameSource.PROVIDER); + + if (!Objects.equals(snapshot.getPreviousUsername(), currentUsername) && snapshot.getPreviousUsername() != null) { + accountPersistenceService.updateUsername(snapshot.getUniqueId(), currentUsername); + historyPersistenceService.record(AccountUsernameHistoryEntry.builder() + .uniqueId(snapshot.getUniqueId()) + .providerId(snapshot.getProviderId()) + .oldUsername(snapshot.getPreviousUsername()) + .newUsername(currentUsername) + .source(AccountUsernameSource.PROVIDER) + .changedAt(System.currentTimeMillis()) + .build()); + eventManager.call(new UsernameChangedEvent( + snapshot.getUniqueId(), + snapshot.getPreviousUsername(), + currentUsername, + snapshot.getProviderId() + )); + } + + return CompletableFuture.completedFuture(PhaseResult.pass(state)); + } + + protected abstract @Nullable ScenarioContext scenarioContext(@NotNull PipelineState pipelineState, @NotNull S state); +} diff --git a/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/pipeline/phase/base/AbstractSynchronizeUsernamePhase.java b/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/pipeline/phase/base/AbstractSynchronizeUsernamePhase.java new file mode 100644 index 000000000..b2684d75c --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/pipeline/phase/base/AbstractSynchronizeUsernamePhase.java @@ -0,0 +1,76 @@ +package me.whereareiam.identica.provider.capability.authoritative.username.pipeline.phase.base; + +import com.google.inject.Provider; +import me.whereareiam.identica.model.config.Messages; +import me.whereareiam.identica.model.identity.Account; +import me.whereareiam.identica.model.identity.provider.AccountProviderLink; +import me.whereareiam.identica.model.identity.provider.AccountProviderProfile; +import me.whereareiam.identica.model.pipeline.PipelineResult; +import me.whereareiam.identica.model.pipeline.phase.PhaseResult; +import me.whereareiam.identica.pipeline.PipelinePhase; +import me.whereareiam.identica.pipeline.ScenarioContext; +import me.whereareiam.identica.pipeline.state.AbstractGroupState; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.provider.ProviderManager; +import me.whereareiam.identica.provider.capability.authoritative.username.database.AccountUsernameStatePersistenceService; +import me.whereareiam.identica.provider.capability.authoritative.username.pipeline.UsernameStateItem; +import me.whereareiam.identica.provider.capability.authoritative.username.type.AccountUsernameSource; +import me.whereareiam.identica.type.pipeline.PipelineStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +public abstract class AbstractSynchronizeUsernamePhase extends AbstractUsernamePhase implements PipelinePhase { + protected final Provider messagesProvider; + + protected AbstractSynchronizeUsernamePhase( + @NotNull ProviderManager providerManager, + @NotNull AccountUsernameStatePersistenceService statePersistenceService, + @NotNull Provider messagesProvider + ) { + super(providerManager, statePersistenceService); + this.messagesProvider = messagesProvider; + } + + @Override + public final @NotNull CompletionStage> execute(@NotNull PipelineState pipelineState, @NotNull S state) { + PipelineResult result = state.getResult(); + if (result == null || result.getStatus() != PipelineStatus.COMPLETE) + return CompletableFuture.completedFuture(PhaseResult.pass(state)); + + ScenarioContext context = scenarioContext(pipelineState, state); + Account account = account(state); + AccountProviderLink link = link(state); + AccountProviderProfile profile = profile(state); + if (context == null || account == null || link == null || profile == null) { + state.setResult(PipelineResult.failed(missingStateMessage())); + return CompletableFuture.completedFuture(PhaseResult.pass(state)); + } + + AccountUsernameSource source = resolveSource(account.getUniqueId()); + String previousUsername = account.getUsername(); + synchronize(account, link, profile); + context.getIdentity().setUsername(account.getUsername()); + pipelineState.putItem(UsernameStateItem.builder() + .uniqueId(account.getUniqueId()) + .previousUsername(previousUsername) + .providerId(link.getProviderId()) + .providerSubject(link.getProviderSubject()) + .source(source) + .build(), 0L); + + return CompletableFuture.completedFuture(PhaseResult.pass(state)); + } + + protected abstract @Nullable ScenarioContext scenarioContext(@NotNull PipelineState pipelineState, @NotNull S state); + + protected abstract @Nullable Account account(@NotNull S state); + + protected abstract @Nullable AccountProviderLink link(@NotNull S state); + + protected abstract @Nullable AccountProviderProfile profile(@NotNull S state); + + protected abstract @NotNull String missingStateMessage(); +} diff --git a/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/pipeline/phase/base/AbstractUsernamePhase.java b/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/pipeline/phase/base/AbstractUsernamePhase.java new file mode 100644 index 000000000..046c103ca --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/pipeline/phase/base/AbstractUsernamePhase.java @@ -0,0 +1,66 @@ +package me.whereareiam.identica.provider.capability.authoritative.username.pipeline.phase.base; + +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import me.whereareiam.identica.model.identity.Account; +import me.whereareiam.identica.model.identity.provider.AccountProviderLink; +import me.whereareiam.identica.model.identity.provider.AccountProviderProfile; +import me.whereareiam.identica.model.provider.InternalProvider; +import me.whereareiam.identica.provider.ProviderManager; +import me.whereareiam.identica.provider.capability.authoritative.username.database.AccountUsernameStatePersistenceService; +import me.whereareiam.identica.provider.capability.authoritative.username.model.account.AccountUsernameState; +import me.whereareiam.identica.provider.capability.authoritative.username.type.AccountUsernameSource; +import me.whereareiam.identica.provider.capability.authoritative.username.type.AuthoritativeUsernameCapability; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; +import java.util.UUID; + +@RequiredArgsConstructor(access = AccessLevel.PROTECTED) +public abstract class AbstractUsernamePhase { + private final ProviderManager providerManager; + private final AccountUsernameStatePersistenceService statePersistenceService; + + protected @NotNull AccountUsernameSource resolveSource(@Nullable UUID uniqueId) { + if (uniqueId == null) return AccountUsernameSource.PROVIDER; + + return statePersistenceService.find(uniqueId) + .map(AccountUsernameState::getSource) + .orElse(AccountUsernameSource.PROVIDER); + } + + protected void saveSource(@Nullable UUID uniqueId, @NotNull AccountUsernameSource source) { + if (uniqueId == null) return; + + statePersistenceService.save(uniqueId, source); + } + + protected boolean synchronize( + @NotNull Account account, + @NotNull AccountProviderLink link, + @NotNull AccountProviderProfile profile + ) { + String providerUsername = profile.getProviderUsername(); + if (providerUsername.isBlank()) return false; + if (!link.isPrimaryLink()) return false; + if (!isAuthoritativeProvider(link.getProviderId())) return false; + if (resolveSource(account.getUniqueId()) == AccountUsernameSource.MANUAL) return false; + + String candidate = providerUsername.trim(); + if (candidate.equals(account.getUsername())) return false; + + account.setUsername(candidate); + return true; + } + + private boolean isAuthoritativeProvider(@Nullable String providerId) { + if (providerId == null || providerId.isBlank()) return false; + + return providerManager.getProviders().stream() + .map(InternalProvider::getDescriptor) + .filter(Objects::nonNull) + .anyMatch(descriptor -> descriptor.getId().equalsIgnoreCase(providerId) + && descriptor.hasCapability(AuthoritativeUsernameCapability.CAPABILITY)); + } +} diff --git a/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/pipeline/prepare/group/policy/phase/ApplyAuthoritativeUsernamePhase.java b/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/pipeline/prepare/group/policy/phase/ApplyAuthoritativeUsernamePhase.java new file mode 100644 index 000000000..e883fd59d --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/pipeline/prepare/group/policy/phase/ApplyAuthoritativeUsernamePhase.java @@ -0,0 +1,111 @@ +package me.whereareiam.identica.provider.capability.authoritative.username.pipeline.prepare.group.policy.phase; + +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import me.whereareiam.identica.model.pipeline.PipelineResult; +import me.whereareiam.identica.model.pipeline.phase.PhaseResult; +import me.whereareiam.identica.model.pipeline.prepare.PrepareAccountCandidateItem; +import me.whereareiam.identica.model.pipeline.prepare.PrepareContextItem; +import me.whereareiam.identica.model.pipeline.prepare.decision.PrepareDecisionItem; +import me.whereareiam.identica.pipeline.PipelinePhase; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.prepare.PrepareGroupState; +import me.whereareiam.identica.provider.ProviderManager; +import me.whereareiam.identica.provider.capability.authoritative.username.conflict.resolver.UsernameConflictResolver; +import me.whereareiam.identica.provider.capability.authoritative.username.database.AccountUsernameStatePersistenceService; +import me.whereareiam.identica.provider.capability.authoritative.username.model.AuthoritativeUsernameMessages; +import me.whereareiam.identica.provider.capability.authoritative.username.model.conflict.UsernameConflictResult; +import me.whereareiam.identica.provider.capability.authoritative.username.model.conflict.UsernameConflictSubject; +import me.whereareiam.identica.provider.capability.authoritative.username.pipeline.phase.base.AbstractUsernamePhase; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +@Singleton +public class ApplyAuthoritativeUsernamePhase extends AbstractUsernamePhase implements PipelinePhase { + private final UsernameConflictResolver conflictResolver; + private final Provider messagesProvider; + + @Inject + public ApplyAuthoritativeUsernamePhase( + ProviderManager providerManager, + AccountUsernameStatePersistenceService statePersistenceService, + UsernameConflictResolver conflictResolver, + Provider messagesProvider + ) { + super(providerManager, statePersistenceService); + this.conflictResolver = conflictResolver; + this.messagesProvider = messagesProvider; + } + + @Override + public @NotNull String id() { + return "apply-authoritative-username"; + } + + @Override + public int order() { + return 100; + } + + @Override + public @NotNull Class stateType() { + return PrepareGroupState.class; + } + + @Override + public boolean supports(@NotNull PipelineState pipelineState, @NotNull PrepareGroupState state) { + if (pipelineState.item(PrepareDecisionItem.class).isPresent()) return false; + + PrepareAccountCandidateItem candidate = pipelineState.item(PrepareAccountCandidateItem.class).orElse(null); + return candidate != null + && candidate.getAccount() != null + && candidate.getLink() != null + && candidate.getProfile() != null; + } + + @Override + public @NotNull CompletionStage> execute( + @NotNull PipelineState pipelineState, + @NotNull PrepareGroupState state + ) { + PrepareContextItem context = pipelineState.item(PrepareContextItem.class).orElse(new PrepareContextItem()); + PrepareAccountCandidateItem candidate = pipelineState.item(PrepareAccountCandidateItem.class).orElse(null); + if (candidate == null || candidate.getAccount() == null || candidate.getLink() == null || candidate.getProfile() == null) { + state.setResult(PipelineResult.failed(join(messagesProvider.get().getPipeline().getPrepare().getFailed()))); + return CompletableFuture.completedFuture(PhaseResult.pass(state)); + } + + synchronize(candidate.getAccount(), candidate.getLink(), candidate.getProfile()); + + UsernameConflictResult result = conflictResolver.handle( + UsernameConflictSubject.builder() + .candidateUsername(candidate.getAccount().getUsername()) + .account(candidate.getAccount()) + .incomingLink(candidate.getLink()) + .incomingProfile(candidate.getProfile()) + .providerContext(context.getProvider()) + .build(), + candidate.getAccount().getUsername() + ); + + candidate.getAccount().setUsername(result.getEffectiveUsername()); + candidate.setEffectiveUsername(result.getEffectiveUsername()); + pipelineState.putItem(candidate, 0L); + if (!result.isDenied()) return CompletableFuture.completedFuture(PhaseResult.pass(state)); + + pipelineState.putItem(PrepareDecisionItem.deny( + result.getDenialMessage(), + context, + candidate.getUniqueId(), + result.getEffectiveUsername() + ), 0L); + return CompletableFuture.completedFuture(PhaseResult.pass(state)); + } + + private @NotNull String join(@NotNull java.util.List lines) { + return String.join("\n", lines); + } +} diff --git a/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/pipeline/scenario/type/authentication/group/identity/phase/SynchronizeAuthenticationUsernamePhase.java b/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/pipeline/scenario/type/authentication/group/identity/phase/SynchronizeAuthenticationUsernamePhase.java new file mode 100644 index 000000000..ea1a432eb --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/pipeline/scenario/type/authentication/group/identity/phase/SynchronizeAuthenticationUsernamePhase.java @@ -0,0 +1,78 @@ +package me.whereareiam.identica.provider.capability.authoritative.username.pipeline.scenario.type.authentication.group.identity.phase; + +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import me.whereareiam.identica.model.auth.AuthContext; +import me.whereareiam.identica.model.config.Messages; +import me.whereareiam.identica.model.identity.Account; +import me.whereareiam.identica.model.identity.provider.AccountProviderLink; +import me.whereareiam.identica.model.identity.provider.AccountProviderProfile; +import me.whereareiam.identica.pipeline.ScenarioContext; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.type.authentication.IdentityState; +import me.whereareiam.identica.provider.ProviderManager; +import me.whereareiam.identica.provider.capability.authoritative.username.database.AccountUsernameStatePersistenceService; +import me.whereareiam.identica.provider.capability.authoritative.username.pipeline.phase.base.AbstractSynchronizeUsernamePhase; +import me.whereareiam.identica.type.pipeline.PipelineType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@Singleton +public class SynchronizeAuthenticationUsernamePhase extends AbstractSynchronizeUsernamePhase { + @Inject + public SynchronizeAuthenticationUsernamePhase( + ProviderManager providerManager, + AccountUsernameStatePersistenceService statePersistenceService, + Provider messagesProvider + ) { + super(providerManager, statePersistenceService, messagesProvider); + } + + @Override + public @NotNull String id() { + return "authoritative-username-sync"; + } + + @Override + public int order() { + return 300; + } + + @Override + public @NotNull Class stateType() { + return IdentityState.class; + } + + @Override + protected @Nullable ScenarioContext scenarioContext(@NotNull PipelineState pipelineState, @NotNull IdentityState state) { + return pipelineState.getScenario(PipelineType.AUTHENTICATION) instanceof AuthContext authContext + ? authContext + : null; + } + + @Override + protected @Nullable Account account(@NotNull IdentityState state) { + return state.getAccount(); + } + + @Override + protected @Nullable AccountProviderLink link(@NotNull IdentityState state) { + return state.getProviderLink(); + } + + @Override + protected @Nullable AccountProviderProfile profile(@NotNull IdentityState state) { + return state.getProfile(); + } + + @Override + protected @NotNull String missingStateMessage() { + return String.join("\n", messagesProvider.get() + .getScenarios() + .getAuthentication() + .getErrors() + .getIdentity() + .getReplicationMissing()); + } +} diff --git a/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/pipeline/scenario/type/authentication/group/policy/phase/PersistAuthenticationUsernamePhase.java b/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/pipeline/scenario/type/authentication/group/policy/phase/PersistAuthenticationUsernamePhase.java new file mode 100644 index 000000000..23faceac1 --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/pipeline/scenario/type/authentication/group/policy/phase/PersistAuthenticationUsernamePhase.java @@ -0,0 +1,52 @@ +package me.whereareiam.identica.provider.capability.authoritative.username.pipeline.scenario.type.authentication.group.policy.phase; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import me.whereareiam.identica.database.AccountPersistenceService; +import me.whereareiam.identica.event.EventManager; +import me.whereareiam.identica.model.auth.AuthContext; +import me.whereareiam.identica.pipeline.ScenarioContext; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.type.authentication.PolicyState; +import me.whereareiam.identica.provider.ProviderManager; +import me.whereareiam.identica.provider.capability.authoritative.username.database.AccountUsernameHistoryPersistenceService; +import me.whereareiam.identica.provider.capability.authoritative.username.database.AccountUsernameStatePersistenceService; +import me.whereareiam.identica.provider.capability.authoritative.username.pipeline.phase.base.AbstractPersistUsernamePhase; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@Singleton +public class PersistAuthenticationUsernamePhase extends AbstractPersistUsernamePhase { + @Inject + public PersistAuthenticationUsernamePhase( + ProviderManager providerManager, + AccountUsernameStatePersistenceService statePersistenceService, + AccountPersistenceService accountPersistenceService, + AccountUsernameHistoryPersistenceService historyPersistenceService, + EventManager eventManager + ) { + super(providerManager, statePersistenceService, accountPersistenceService, historyPersistenceService, eventManager); + } + + @Override + public @NotNull String id() { + return "persist-authoritative-username"; + } + + @Override + public int order() { + return 200; + } + + @Override + public @NotNull Class stateType() { + return PolicyState.class; + } + + @Override + protected @Nullable ScenarioContext scenarioContext(@NotNull PipelineState pipelineState, @NotNull PolicyState state) { + return pipelineState.getScenario(pipelineState.getPipelineType()) instanceof AuthContext authContext + ? authContext + : null; + } +} diff --git a/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/pipeline/scenario/type/authentication/group/policy/phase/ReviewAuthenticationUsernamePhase.java b/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/pipeline/scenario/type/authentication/group/policy/phase/ReviewAuthenticationUsernamePhase.java new file mode 100644 index 000000000..b595129ca --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/pipeline/scenario/type/authentication/group/policy/phase/ReviewAuthenticationUsernamePhase.java @@ -0,0 +1,104 @@ +package me.whereareiam.identica.provider.capability.authoritative.username.pipeline.scenario.type.authentication.group.policy.phase; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import lombok.RequiredArgsConstructor; +import me.whereareiam.identica.database.AccountPersistenceService; +import me.whereareiam.identica.database.provider.ProviderLinkPersistenceService; +import me.whereareiam.identica.database.provider.ProviderProfilePersistenceService; +import me.whereareiam.identica.model.auth.AuthContext; +import me.whereareiam.identica.model.identity.Account; +import me.whereareiam.identica.model.identity.provider.AccountProviderLink; +import me.whereareiam.identica.model.identity.provider.AccountProviderProfile; +import me.whereareiam.identica.model.pipeline.PipelineResult; +import me.whereareiam.identica.model.pipeline.phase.PhaseResult; +import me.whereareiam.identica.pipeline.PipelinePhase; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.type.authentication.PolicyState; +import me.whereareiam.identica.provider.capability.authoritative.username.conflict.resolver.UsernameConflictResolver; +import me.whereareiam.identica.provider.capability.authoritative.username.model.conflict.UsernameConflictResult; +import me.whereareiam.identica.provider.capability.authoritative.username.model.conflict.UsernameConflictSubject; +import me.whereareiam.identica.provider.capability.authoritative.username.pipeline.UsernameStateItem; +import me.whereareiam.identica.type.pipeline.PipelineStatus; +import me.whereareiam.identica.type.pipeline.PipelineType; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +@Singleton +@RequiredArgsConstructor(onConstructor_ = @Inject) +public class ReviewAuthenticationUsernamePhase implements PipelinePhase { + private final UsernameConflictResolver conflictResolver; + private final AccountPersistenceService accountPersistenceService; + private final ProviderLinkPersistenceService providerLinkPersistenceService; + private final ProviderProfilePersistenceService providerProfilePersistenceService; + + @Override + public @NotNull String id() { + return "review-authoritative-username"; + } + + @Override + public int order() { + return 100; + } + + @Override + public @NotNull Class stateType() { + return PolicyState.class; + } + + @Override + public @NotNull CompletionStage> execute(@NotNull PipelineState pipelineState, @NotNull PolicyState state) { + PipelineResult result = state.getResult(); + if (result == null || result.getStatus() != PipelineStatus.COMPLETE) { + return CompletableFuture.completedFuture(PhaseResult.pass(state)); + } + + AuthContext context = pipelineState.getScenario(PipelineType.AUTHENTICATION) instanceof AuthContext authContext + ? authContext + : null; + UsernameStateItem snapshot = pipelineState.item(UsernameStateItem.class).orElse(null); + if (context == null || snapshot == null || snapshot.getUniqueId() == null + || snapshot.getProviderId() == null || snapshot.getProviderSubject() == null) + return CompletableFuture.completedFuture(PhaseResult.pass(state)); + + Account account = accountPersistenceService.findByUniqueId(snapshot.getUniqueId()) + .map(found -> found.toBuilder().username(context.getUsername()).build()) + .orElse(Account.builder() + .uniqueId(snapshot.getUniqueId()) + .username(context.getUsername()) + .build()); + AccountProviderLink link = providerLinkPersistenceService.findBySubject(snapshot.getProviderId(), snapshot.getProviderSubject()) + .orElseGet(() -> AccountProviderLink.builder() + .uniqueId(snapshot.getUniqueId()) + .providerId(snapshot.getProviderId()) + .providerSubject(snapshot.getProviderSubject()) + .primaryLink(true) + .build()); + AccountProviderProfile profile = providerProfilePersistenceService.findBySubject(snapshot.getProviderId(), snapshot.getProviderSubject()) + .orElseGet(() -> AccountProviderProfile.builder() + .providerId(snapshot.getProviderId()) + .providerSubject(snapshot.getProviderSubject()) + .providerUsername(context.getProvider() != null + ? context.getProvider().getProviderUsername() + : context.getUsername()) + .build()); + + UsernameConflictResult conflictResult = conflictResolver.handle( + UsernameConflictSubject.builder() + .candidateUsername(account.getUsername()) + .account(account) + .incomingLink(link) + .incomingProfile(profile) + .providerContext(context.getProvider()) + .build(), + context.getUsername() + ); + context.getIdentity().setUsername(conflictResult.getEffectiveUsername()); + if (conflictResult.isDenied()) state.setResult(PipelineResult.denied(conflictResult.getDenialMessage())); + + return CompletableFuture.completedFuture(PhaseResult.pass(state)); + } +} diff --git a/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/pipeline/scenario/type/migration/group/identity/phase/SynchronizeMigrationUsernamePhase.java b/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/pipeline/scenario/type/migration/group/identity/phase/SynchronizeMigrationUsernamePhase.java new file mode 100644 index 000000000..00410c667 --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/pipeline/scenario/type/migration/group/identity/phase/SynchronizeMigrationUsernamePhase.java @@ -0,0 +1,103 @@ +package me.whereareiam.identica.provider.capability.authoritative.username.pipeline.scenario.type.migration.group.identity.phase; + +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import me.whereareiam.identica.database.AccountPersistenceService; +import me.whereareiam.identica.event.EventManager; +import me.whereareiam.identica.model.config.Messages; +import me.whereareiam.identica.model.pipeline.PipelineResult; +import me.whereareiam.identica.model.pipeline.phase.PhaseResult; +import me.whereareiam.identica.pipeline.PipelinePhase; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.type.migration.IdentityState; +import me.whereareiam.identica.provider.ProviderManager; +import me.whereareiam.identica.provider.capability.authoritative.username.database.AccountUsernameHistoryPersistenceService; +import me.whereareiam.identica.provider.capability.authoritative.username.database.AccountUsernameStatePersistenceService; +import me.whereareiam.identica.provider.capability.authoritative.username.event.UsernameChangedEvent; +import me.whereareiam.identica.provider.capability.authoritative.username.model.account.AccountUsernameHistoryEntry; +import me.whereareiam.identica.provider.capability.authoritative.username.pipeline.phase.base.AbstractUsernamePhase; +import me.whereareiam.identica.provider.capability.authoritative.username.type.AccountUsernameSource; +import me.whereareiam.identica.type.pipeline.PipelineStatus; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +@Singleton +public class SynchronizeMigrationUsernamePhase extends AbstractUsernamePhase implements PipelinePhase { + private final AccountPersistenceService accountPersistenceService; + private final AccountUsernameHistoryPersistenceService historyPersistenceService; + private final EventManager eventManager; + private final Provider messagesProvider; + + @Inject + public SynchronizeMigrationUsernamePhase( + ProviderManager providerManager, + AccountUsernameStatePersistenceService statePersistenceService, + AccountPersistenceService accountPersistenceService, + AccountUsernameHistoryPersistenceService historyPersistenceService, + EventManager eventManager, + Provider messagesProvider + ) { + super(providerManager, statePersistenceService); + this.accountPersistenceService = accountPersistenceService; + this.historyPersistenceService = historyPersistenceService; + this.eventManager = eventManager; + this.messagesProvider = messagesProvider; + } + + @Override + public @NotNull String id() { + return "authoritative-username-sync"; + } + + @Override + public int order() { + return 500; + } + + @Override + public @NotNull Class stateType() { + return IdentityState.class; + } + + @Override + public @NotNull CompletionStage> execute(@NotNull PipelineState pipelineState, @NotNull IdentityState state) { + PipelineResult result = state.getResult(); + if (result == null || result.getStatus() != PipelineStatus.COMPLETE) { + return CompletableFuture.completedFuture(PhaseResult.pass(state)); + } + + if (state.getContext() == null || state.getAccount() == null || state.getLink() == null || state.getProfile() == null) { + state.setResult(PipelineResult.failed(String.join("\n", messagesProvider.get().getScenarios().getMigration().getErrors().getIdentity().getReplicationMissing()))); + return CompletableFuture.completedFuture(PhaseResult.pass(state)); + } + + String previousUsername = state.getAccount().getUsername(); + boolean changed = synchronize(state.getAccount(), state.getLink(), state.getProfile()); + state.getContext().getIdentity().setUsername(state.getAccount().getUsername()); + saveSource(state.getAccount().getUniqueId(), AccountUsernameSource.PROVIDER); + + if (changed) applyUsernameChange(state, previousUsername); + return CompletableFuture.completedFuture(PhaseResult.pass(state)); + } + + private void applyUsernameChange(@NotNull IdentityState state, @NotNull String previousUsername) { + accountPersistenceService.updateUsername(state.getAccount().getUniqueId(), state.getAccount().getUsername()); + historyPersistenceService.record(AccountUsernameHistoryEntry.builder() + .uniqueId(state.getAccount().getUniqueId()) + .providerId(state.getLink().getProviderId()) + .oldUsername(previousUsername) + .newUsername(state.getAccount().getUsername()) + .source(AccountUsernameSource.PROVIDER) + .changedAt(System.currentTimeMillis()) + .build()); + eventManager.call(new UsernameChangedEvent( + state.getAccount().getUniqueId(), + previousUsername, + state.getAccount().getUsername(), + state.getLink().getProviderId() + )); + } +} diff --git a/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/pipeline/scenario/type/registration/group/identity/phase/SynchronizeRegistrationUsernamePhase.java b/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/pipeline/scenario/type/registration/group/identity/phase/SynchronizeRegistrationUsernamePhase.java new file mode 100644 index 000000000..95e1f7527 --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/pipeline/scenario/type/registration/group/identity/phase/SynchronizeRegistrationUsernamePhase.java @@ -0,0 +1,69 @@ +package me.whereareiam.identica.provider.capability.authoritative.username.pipeline.scenario.type.registration.group.identity.phase; + +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import me.whereareiam.identica.model.config.Messages; +import me.whereareiam.identica.model.identity.Account; +import me.whereareiam.identica.model.identity.provider.AccountProviderLink; +import me.whereareiam.identica.model.identity.provider.AccountProviderProfile; +import me.whereareiam.identica.pipeline.ScenarioContext; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.type.registration.IdentityState; +import me.whereareiam.identica.provider.ProviderManager; +import me.whereareiam.identica.provider.capability.authoritative.username.database.AccountUsernameStatePersistenceService; +import me.whereareiam.identica.provider.capability.authoritative.username.pipeline.phase.base.AbstractSynchronizeUsernamePhase; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@Singleton +public class SynchronizeRegistrationUsernamePhase extends AbstractSynchronizeUsernamePhase { + @Inject + public SynchronizeRegistrationUsernamePhase( + ProviderManager providerManager, + AccountUsernameStatePersistenceService statePersistenceService, + Provider messagesProvider + ) { + super(providerManager, statePersistenceService, messagesProvider); + } + + @Override + public @NotNull String id() { + return "authoritative-username-sync"; + } + + @Override + public int order() { + return 500; + } + + @Override + public @NotNull Class stateType() { + return IdentityState.class; + } + + @Override + protected @Nullable ScenarioContext scenarioContext(@NotNull PipelineState pipelineState, @NotNull IdentityState state) { + return state.getContext(); + } + + @Override + protected @Nullable Account account(@NotNull IdentityState state) { + return state.getAccount(); + } + + @Override + protected @Nullable AccountProviderLink link(@NotNull IdentityState state) { + return state.getLink(); + } + + @Override + protected @Nullable AccountProviderProfile profile(@NotNull IdentityState state) { + return state.getProfile(); + } + + @Override + protected @NotNull String missingStateMessage() { + return String.join("\n", messagesProvider.get().getScenarios().getRegistration().getErrors().getIdentity().getReplicationMissing()); + } +} diff --git a/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/pipeline/scenario/type/registration/group/policy/phase/PersistRegistrationUsernamePhase.java b/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/pipeline/scenario/type/registration/group/policy/phase/PersistRegistrationUsernamePhase.java new file mode 100644 index 000000000..23debf471 --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/pipeline/scenario/type/registration/group/policy/phase/PersistRegistrationUsernamePhase.java @@ -0,0 +1,52 @@ +package me.whereareiam.identica.provider.capability.authoritative.username.pipeline.scenario.type.registration.group.policy.phase; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import me.whereareiam.identica.database.AccountPersistenceService; +import me.whereareiam.identica.event.EventManager; +import me.whereareiam.identica.model.registration.RegistrationContext; +import me.whereareiam.identica.pipeline.ScenarioContext; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.type.registration.PolicyState; +import me.whereareiam.identica.provider.ProviderManager; +import me.whereareiam.identica.provider.capability.authoritative.username.database.AccountUsernameHistoryPersistenceService; +import me.whereareiam.identica.provider.capability.authoritative.username.database.AccountUsernameStatePersistenceService; +import me.whereareiam.identica.provider.capability.authoritative.username.pipeline.phase.base.AbstractPersistUsernamePhase; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@Singleton +public class PersistRegistrationUsernamePhase extends AbstractPersistUsernamePhase { + @Inject + public PersistRegistrationUsernamePhase( + ProviderManager providerManager, + AccountUsernameStatePersistenceService statePersistenceService, + AccountPersistenceService accountPersistenceService, + AccountUsernameHistoryPersistenceService historyPersistenceService, + EventManager eventManager + ) { + super(providerManager, statePersistenceService, accountPersistenceService, historyPersistenceService, eventManager); + } + + @Override + public @NotNull String id() { + return "persist-authoritative-username"; + } + + @Override + public int order() { + return 200; + } + + @Override + public @NotNull Class stateType() { + return PolicyState.class; + } + + @Override + protected @Nullable ScenarioContext scenarioContext(@NotNull PipelineState pipelineState, @NotNull PolicyState state) { + return pipelineState.getScenario(pipelineState.getPipelineType()) instanceof RegistrationContext registrationContext + ? registrationContext + : null; + } +} diff --git a/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/pipeline/scenario/type/registration/group/policy/phase/ReviewRegistrationUsernamePhase.java b/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/pipeline/scenario/type/registration/group/policy/phase/ReviewRegistrationUsernamePhase.java new file mode 100644 index 000000000..bb8a48e3c --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-common/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/pipeline/scenario/type/registration/group/policy/phase/ReviewRegistrationUsernamePhase.java @@ -0,0 +1,103 @@ +package me.whereareiam.identica.provider.capability.authoritative.username.pipeline.scenario.type.registration.group.policy.phase; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import lombok.RequiredArgsConstructor; +import me.whereareiam.identica.database.AccountPersistenceService; +import me.whereareiam.identica.database.provider.ProviderLinkPersistenceService; +import me.whereareiam.identica.database.provider.ProviderProfilePersistenceService; +import me.whereareiam.identica.model.identity.Account; +import me.whereareiam.identica.model.identity.provider.AccountProviderLink; +import me.whereareiam.identica.model.identity.provider.AccountProviderProfile; +import me.whereareiam.identica.model.pipeline.PipelineResult; +import me.whereareiam.identica.model.pipeline.phase.PhaseResult; +import me.whereareiam.identica.model.registration.RegistrationContext; +import me.whereareiam.identica.pipeline.PipelinePhase; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.type.registration.PolicyState; +import me.whereareiam.identica.provider.capability.authoritative.username.conflict.resolver.UsernameConflictResolver; +import me.whereareiam.identica.provider.capability.authoritative.username.model.conflict.UsernameConflictResult; +import me.whereareiam.identica.provider.capability.authoritative.username.model.conflict.UsernameConflictSubject; +import me.whereareiam.identica.provider.capability.authoritative.username.pipeline.UsernameStateItem; +import me.whereareiam.identica.type.pipeline.PipelineStatus; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +@Singleton +@RequiredArgsConstructor(onConstructor_ = @Inject) +public class ReviewRegistrationUsernamePhase implements PipelinePhase { + private final UsernameConflictResolver conflictResolver; + private final AccountPersistenceService accountPersistenceService; + private final ProviderLinkPersistenceService providerLinkPersistenceService; + private final ProviderProfilePersistenceService providerProfilePersistenceService; + + @Override + public @NotNull String id() { + return "review-authoritative-username"; + } + + @Override + public int order() { + return 100; + } + + @Override + public @NotNull Class stateType() { + return PolicyState.class; + } + + @Override + public @NotNull CompletionStage> execute(@NotNull PipelineState pipelineState, @NotNull PolicyState state) { + PipelineResult result = state.getResult(); + if (result == null || result.getStatus() != PipelineStatus.COMPLETE) { + return CompletableFuture.completedFuture(PhaseResult.pass(state)); + } + + RegistrationContext context = pipelineState.getScenario(pipelineState.getPipelineType()) instanceof RegistrationContext registrationContext + ? registrationContext + : null; + UsernameStateItem snapshot = pipelineState.item(UsernameStateItem.class).orElse(null); + if (snapshot == null || context == null || snapshot.getUniqueId() == null + || snapshot.getProviderId() == null || snapshot.getProviderSubject() == null) + return CompletableFuture.completedFuture(PhaseResult.pass(state)); + + Account account = accountPersistenceService.findByUniqueId(snapshot.getUniqueId()) + .map(found -> found.toBuilder().username(context.getUsername()).build()) + .orElse(Account.builder() + .uniqueId(snapshot.getUniqueId()) + .username(context.getUsername()) + .build()); + AccountProviderLink link = providerLinkPersistenceService.findBySubject(snapshot.getProviderId(), snapshot.getProviderSubject()) + .orElseGet(() -> AccountProviderLink.builder() + .uniqueId(snapshot.getUniqueId()) + .providerId(snapshot.getProviderId()) + .providerSubject(snapshot.getProviderSubject()) + .primaryLink(true) + .build()); + AccountProviderProfile profile = providerProfilePersistenceService.findBySubject(snapshot.getProviderId(), snapshot.getProviderSubject()) + .orElseGet(() -> AccountProviderProfile.builder() + .providerId(snapshot.getProviderId()) + .providerSubject(snapshot.getProviderSubject()) + .providerUsername(context.getProvider() != null + ? context.getProvider().getProviderUsername() + : context.getUsername()) + .build()); + + UsernameConflictResult conflictResult = conflictResolver.handle( + UsernameConflictSubject.builder() + .candidateUsername(account.getUsername()) + .account(account) + .incomingLink(link) + .incomingProfile(profile) + .providerContext(context.getProvider()) + .build(), + context.getUsername() + ); + context.getIdentity().setUsername(conflictResult.getEffectiveUsername()); + if (conflictResult.isDenied()) state.setResult(PipelineResult.denied(conflictResult.getDenialMessage())); + + return CompletableFuture.completedFuture(PhaseResult.pass(state)); + } +} diff --git a/identica-capability/capability-authoritative-username/authoritative-username-common/src/test/java/me/whereareiam/identica/provider/capability/authoritative/username/conflict/resolver/FormatUsernameConflictResolverTest.java b/identica-capability/capability-authoritative-username/authoritative-username-common/src/test/java/me/whereareiam/identica/provider/capability/authoritative/username/conflict/resolver/FormatUsernameConflictResolverTest.java new file mode 100644 index 000000000..ea1db8960 --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-common/src/test/java/me/whereareiam/identica/provider/capability/authoritative/username/conflict/resolver/FormatUsernameConflictResolverTest.java @@ -0,0 +1,88 @@ +package me.whereareiam.identica.provider.capability.authoritative.username.conflict.resolver; + +import me.whereareiam.identica.model.conflict.ConflictContext; +import me.whereareiam.identica.model.conflict.ConflictResolution; +import me.whereareiam.identica.model.conflict.participant.ConflictParticipant; +import me.whereareiam.identica.model.conflict.participant.ConflictParticipantRole; +import me.whereareiam.identica.provider.ProviderOperations; +import me.whereareiam.identica.provider.capability.authoritative.username.UsernameConflictSchema; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@DisplayName("Format Username Conflict Resolver") +class FormatUsernameConflictResolverTest { + @DisplayName("Target both emits incoming and existing effects") + @Test + void targetBothEmitsIncomingAndExistingEffects() { + ProviderOperations operations = mock(ProviderOperations.class); + when(operations.displayProviderName("premium")).thenReturn("Premium"); + when(operations.displayProviderName("credential")).thenReturn("Credential"); + + UsernameFormatResolver resolver = new UsernameFormatResolver(operations); + UsernameFormatResolver.Config config = new UsernameFormatResolver.Config(); + UsernameFormatResolver.Config.Format format = new UsernameFormatResolver.Config.Format(); + format.setPattern("{username}_{incomingProvider}"); + config.setFormat(format); + config.setTarget("both"); + + ConflictResolution resolution = resolver.resolve(context(), config); + assertEquals(ConflictResolution.Decision.ALLOW, resolution.getDecision()); + assertEquals( + "Player_Premium", + resolution.getEffect(UsernameConflictSchema.EFFECT_INCOMING_EFFECTIVE_USERNAME, String.class) + ); + assertEquals( + "Player_Premium", + resolution.getEffect(UsernameConflictSchema.EFFECT_EXISTING_EFFECTIVE_USERNAME, String.class) + ); + } + + @DisplayName("Existing target only emits existing effect") + @Test + void existingTargetOnlyEmitsExistingEffect() { + ProviderOperations operations = mock(ProviderOperations.class); + when(operations.displayProviderName("premium")).thenReturn("Premium"); + when(operations.displayProviderName("credential")).thenReturn("Credential"); + + UsernameFormatResolver resolver = new UsernameFormatResolver(operations); + UsernameFormatResolver.Config config = new UsernameFormatResolver.Config(); + UsernameFormatResolver.Config.Format format = new UsernameFormatResolver.Config.Format(); + format.setPattern("{username}_{existingProvider}"); + config.setFormat(format); + config.setTarget("existing"); + + ConflictResolution resolution = resolver.resolve(context(), config); + assertNull(resolution.getEffect(UsernameConflictSchema.EFFECT_INCOMING_EFFECTIVE_USERNAME, String.class)); + assertEquals( + "Player_Credential", + resolution.getEffect(UsernameConflictSchema.EFFECT_EXISTING_EFFECTIVE_USERNAME, String.class) + ); + } + + private ConflictContext context() { + ConflictContext context = ConflictContext.builder() + .key("username") + .hook("prepare") + .build(); + context.putAttribute(UsernameConflictSchema.ATTRIBUTE_CANDIDATE_USERNAME, "Player"); + context.putParticipant(participant(UsernameConflictSchema.ROLE_INCOMING, "premium")); + context.putParticipant(participant(UsernameConflictSchema.ROLE_EXISTING, "credential")); + return context; + } + + private ConflictParticipant participant( + ConflictParticipantRole role, + String providerId + ) { + ConflictParticipant participant = ConflictParticipant.builder() + .role(role) + .build(); + participant.putAttribute(UsernameConflictSchema.PARTICIPANT_ATTRIBUTE_PROVIDER_ID, providerId); + return participant; + } +} diff --git a/identica-capability/capability-authoritative-username/authoritative-username-common/src/test/java/me/whereareiam/identica/provider/capability/authoritative/username/pipeline/AuthoritativeUsernameConflictPhasesTest.java b/identica-capability/capability-authoritative-username/authoritative-username-common/src/test/java/me/whereareiam/identica/provider/capability/authoritative/username/pipeline/AuthoritativeUsernameConflictPhasesTest.java new file mode 100644 index 000000000..1c18708d2 --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-common/src/test/java/me/whereareiam/identica/provider/capability/authoritative/username/pipeline/AuthoritativeUsernameConflictPhasesTest.java @@ -0,0 +1,234 @@ +package me.whereareiam.identica.provider.capability.authoritative.username.pipeline; + +import me.whereareiam.identica.database.AccountPersistenceService; +import me.whereareiam.identica.database.provider.ProviderLinkPersistenceService; +import me.whereareiam.identica.database.provider.ProviderProfilePersistenceService; +import me.whereareiam.identica.identity.actor.ConnectionIdentity; +import me.whereareiam.identica.model.auth.AuthContext; +import me.whereareiam.identica.model.identity.Account; +import me.whereareiam.identica.model.identity.provider.AccountProviderLink; +import me.whereareiam.identica.model.identity.provider.AccountProviderProfile; +import me.whereareiam.identica.model.pipeline.PipelineResult; +import me.whereareiam.identica.model.pipeline.prepare.PrepareAccountCandidateItem; +import me.whereareiam.identica.model.pipeline.prepare.PrepareContextItem; +import me.whereareiam.identica.model.pipeline.prepare.decision.PrepareDecisionItem; +import me.whereareiam.identica.model.provider.InternalProvider; +import me.whereareiam.identica.model.provider.ProviderContext; +import me.whereareiam.identica.model.provider.ProviderDescriptor; +import me.whereareiam.identica.model.registration.RegistrationContext; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.prepare.PrepareGroupState; +import me.whereareiam.identica.pipeline.state.scenario.type.registration.PolicyState; +import me.whereareiam.identica.provider.ProviderManager; +import me.whereareiam.identica.provider.capability.authoritative.username.conflict.resolver.UsernameConflictResolver; +import me.whereareiam.identica.provider.capability.authoritative.username.database.AccountUsernameStatePersistenceService; +import me.whereareiam.identica.provider.capability.authoritative.username.model.AuthoritativeUsernameMessages; +import me.whereareiam.identica.provider.capability.authoritative.username.model.conflict.UsernameConflictResult; +import me.whereareiam.identica.provider.capability.authoritative.username.pipeline.prepare.group.policy.phase.ApplyAuthoritativeUsernamePhase; +import me.whereareiam.identica.provider.capability.authoritative.username.pipeline.scenario.type.authentication.group.policy.phase.ReviewAuthenticationUsernamePhase; +import me.whereareiam.identica.provider.capability.authoritative.username.pipeline.scenario.type.registration.group.policy.phase.ReviewRegistrationUsernamePhase; +import me.whereareiam.identica.provider.capability.authoritative.username.type.AccountUsernameSource; +import me.whereareiam.identica.provider.capability.authoritative.username.type.AuthoritativeUsernameCapability; +import me.whereareiam.identica.type.pipeline.PipelineType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.*; + +@DisplayName("Authoritative Username Conflict Phases") +class AuthoritativeUsernameConflictPhasesTest { + @DisplayName("Prepare phase applies the handler result and stores a denial decision") + @Test + void preparePhaseAppliesHandlerResult() { + UsernameConflictResolver resolver = mock(UsernameConflictResolver.class); + when(resolver.handle(any(), eq("Player"))).thenReturn( + UsernameConflictResult.denied("denied", "Resolved") + ); + + ApplyAuthoritativeUsernamePhase phase = new ApplyAuthoritativeUsernamePhase( + providerManager(), + statePersistenceService(), + resolver, + this::messages + ); + + PipelineState pipelineState = PipelineState.initial(); + pipelineState.putItem(new PrepareContextItem(providerContext(), null, null), 0L); + pipelineState.putItem(candidate(), 0L); + + phase.execute(pipelineState, new PrepareGroupState()).toCompletableFuture().join(); + + PrepareAccountCandidateItem candidate = pipelineState.item(PrepareAccountCandidateItem.class).orElseThrow(); + PrepareDecisionItem decision = pipelineState.item(PrepareDecisionItem.class).orElseThrow(); + assertEquals("Resolved", candidate.getEffectiveUsername()); + assertEquals("Resolved", candidate.getAccount().getUsername()); + assertEquals("denied", decision.getDenialMessage()); + } + + @DisplayName("Registration policy phase updates the identity username and denial state") + @Test + void registrationPhaseAppliesHandlerResult() { + UsernameConflictResolver resolver = mock(UsernameConflictResolver.class); + when(resolver.handle(any(), eq("Player"))).thenReturn( + UsernameConflictResult.denied("denied", "Resolved") + ); + + ReviewRegistrationUsernamePhase phase = new ReviewRegistrationUsernamePhase( + resolver, + accountServiceEmpty(), + linkServiceEmpty(), + profileServiceEmpty() + ); + + PipelineState pipelineState = PipelineState.initial(); + pipelineState.setPipelineType(PipelineType.REGISTRATION); + RegistrationContext context = RegistrationContext.builder() + .identity(new ConnectionIdentity("Player", "127.0.0.1")) + .provider(providerContext()) + .build(); + pipelineState.setScenario(context); + pipelineState.putItem(snapshot(), 0L); + + PolicyState state = new PolicyState(); + state.setResult(PipelineResult.complete()); + phase.execute(pipelineState, state).toCompletableFuture().join(); + + assertNotNull(state.getResult()); + assertEquals("denied", state.getResult().getMessage()); + verify(resolver).handle(any(), eq("Player")); + } + + @DisplayName("Authentication policy phase updates the identity username") + @Test + void authenticationPhaseAppliesHandlerResult() { + UsernameConflictResolver resolver = mock(UsernameConflictResolver.class); + when(resolver.handle(any(), eq("Player"))).thenReturn( + UsernameConflictResult.allowed("Resolved") + ); + + ReviewAuthenticationUsernamePhase phase = new ReviewAuthenticationUsernamePhase( + resolver, + accountServiceEmpty(), + linkServiceEmpty(), + profileServiceEmpty() + ); + + PipelineState pipelineState = PipelineState.initial(); + pipelineState.setPipelineType(PipelineType.AUTHENTICATION); + AuthContext context = AuthContext.builder() + .identity(new ConnectionIdentity("Player", "127.0.0.1")) + .provider(providerContext()) + .build(); + pipelineState.setScenario(context); + pipelineState.putItem(snapshot(), 0L); + + PolicyState state = new PolicyState(); + state.setResult(PipelineResult.complete()); + phase.execute(pipelineState, state).toCompletableFuture().join(); + + verify(resolver).handle(any(), eq("Player")); + } + + private PrepareAccountCandidateItem candidate() { + PrepareAccountCandidateItem candidate = new PrepareAccountCandidateItem(); + candidate.setUniqueId(UUID.randomUUID()); + candidate.setAccount(Account.builder().uniqueId(candidate.getUniqueId()).username("Player").build()); + candidate.setLink(AccountProviderLink.builder() + .uniqueId(candidate.getUniqueId()) + .providerId("premium") + .providerSubject("subject") + .primaryLink(true) + .build()); + candidate.setProfile(AccountProviderProfile.builder() + .providerId("premium") + .providerSubject("subject") + .providerUsername("Player") + .build()); + return candidate; + } + + private UsernameStateItem snapshot() { + return UsernameStateItem.builder() + .uniqueId(UUID.randomUUID()) + .providerId("premium") + .providerSubject("subject") + .source(AccountUsernameSource.PROVIDER) + .build(); + } + + private ProviderLinkPersistenceService linkServiceEmpty() { + ProviderLinkPersistenceService service = mock(ProviderLinkPersistenceService.class); + when(service.findBySubject(anyString(), anyString())).thenReturn(Optional.empty()); + return service; + } + + private ProviderProfilePersistenceService profileServiceEmpty() { + ProviderProfilePersistenceService service = mock(ProviderProfilePersistenceService.class); + when(service.findBySubject(anyString(), anyString())).thenReturn(Optional.empty()); + return service; + } + + private AccountPersistenceService accountServiceEmpty() { + AccountPersistenceService service = mock(AccountPersistenceService.class); + when(service.findByUniqueId(any())).thenReturn(Optional.empty()); + return service; + } + + private AccountUsernameStatePersistenceService statePersistenceService() { + AccountUsernameStatePersistenceService service = mock(AccountUsernameStatePersistenceService.class); + when(service.find(any())).thenReturn(Optional.empty()); + return service; + } + + private ProviderManager providerManager() { + ProviderManager providerManager = mock(ProviderManager.class); + ProviderDescriptor descriptor = new ProviderDescriptor(); + descriptor.setId("premium"); + descriptor.setName("Premium"); + descriptor.setVersion("1"); + descriptor.setMain("Main"); + descriptor.setSupportedPlatforms(List.of("velocity")); + descriptor.setDeclaredCapabilityIds(List.of(AuthoritativeUsernameCapability.CAPABILITY.getId())); + + InternalProvider provider = InternalProvider.builder() + .descriptor(descriptor) + .state(me.whereareiam.identica.type.provider.ProviderState.ENABLED) + .build(); + when(providerManager.getProviders()).thenReturn(List.of(provider)); + return providerManager; + } + + private ProviderContext providerContext() { + return ProviderContext.builder() + .providerId("premium") + .providerSubject("subject") + .providerUsername("Player") + .build(); + } + + private AuthoritativeUsernameMessages messages() { + AuthoritativeUsernameMessages.Pipeline.Prepare prepare = new AuthoritativeUsernameMessages.Pipeline.Prepare(); + prepare.setFailed(List.of("failed")); + AuthoritativeUsernameMessages.Pipeline.Identity identity = new AuthoritativeUsernameMessages.Pipeline.Identity(); + identity.setSynchronizationFailed(List.of("sync")); + AuthoritativeUsernameMessages.Pipeline.Policy policy = new AuthoritativeUsernameMessages.Pipeline.Policy(); + policy.setPersistenceFailed(List.of("persist")); + policy.setConflictDenied(List.of("denied")); + policy.setEntrypointRequired(List.of("entrypoint")); + + AuthoritativeUsernameMessages.Pipeline pipeline = new AuthoritativeUsernameMessages.Pipeline(); + pipeline.setPrepare(prepare); + pipeline.setIdentity(identity); + pipeline.setPolicy(policy); + + AuthoritativeUsernameMessages messages = new AuthoritativeUsernameMessages(); + messages.setPipeline(pipeline); + return messages; + } +} diff --git a/identica-capability/capability-authoritative-username/authoritative-username-common/src/test/java/me/whereareiam/identica/provider/capability/authoritative/username/service/AuthoritativeUsernameConflictResolverTest.java b/identica-capability/capability-authoritative-username/authoritative-username-common/src/test/java/me/whereareiam/identica/provider/capability/authoritative/username/service/AuthoritativeUsernameConflictResolverTest.java new file mode 100644 index 000000000..bb4d1a245 --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-common/src/test/java/me/whereareiam/identica/provider/capability/authoritative/username/service/AuthoritativeUsernameConflictResolverTest.java @@ -0,0 +1,86 @@ +package me.whereareiam.identica.provider.capability.authoritative.username.service; + +import me.whereareiam.identica.conflict.ConflictService; +import me.whereareiam.identica.identity.session.SessionService; +import me.whereareiam.identica.model.conflict.ConflictContext; +import me.whereareiam.identica.model.conflict.ConflictResolution; +import me.whereareiam.identica.provider.capability.authoritative.username.UsernameConflictSchema; +import me.whereareiam.identica.provider.capability.authoritative.username.conflict.UsernameConflictType; +import me.whereareiam.identica.provider.capability.authoritative.username.conflict.resolver.UsernameConflictResolver; +import me.whereareiam.identica.provider.capability.authoritative.username.model.AuthoritativeUsernameMessages; +import me.whereareiam.identica.provider.capability.authoritative.username.model.conflict.UsernameConflictResult; +import me.whereareiam.identica.provider.capability.authoritative.username.model.conflict.UsernameConflictSubject; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.*; + +@DisplayName("Username Conflict Resolver") +class AuthoritativeUsernameConflictResolverTest { + @DisplayName("Applies incoming and existing effects before denying") + @Test + void appliesEffectsBeforeDenying() { + ConflictService conflictService = mock(ConflictService.class); + UsernameConflictType conflictType = mock(UsernameConflictType.class); + SessionService sessionService = mock(SessionService.class); + + ConflictContext context = ConflictContext.builder() + .key("username") + .hook("prepare") + .build(); + java.util.UUID uniqueId = java.util.UUID.randomUUID(); + me.whereareiam.identica.model.conflict.participant.ConflictParticipant existing = + me.whereareiam.identica.model.conflict.participant.ConflictParticipant.builder() + .role(UsernameConflictSchema.ROLE_EXISTING) + .build(); + existing.putAttribute(UsernameConflictSchema.PARTICIPANT_ATTRIBUTE_ACCOUNT_UNIQUE_ID, uniqueId); + context.putParticipant(existing); + when(conflictType.createContext(any())).thenReturn(context); + when(conflictService.resolve(context)).thenReturn(ConflictResolution.deny(null) + .withEffect(UsernameConflictSchema.EFFECT_CLOSE_EXISTING, true) + .withEffect(UsernameConflictSchema.EFFECT_EXISTING_EFFECTIVE_USERNAME, "Existing_1") + .withEffect(UsernameConflictSchema.EFFECT_INCOMING_EFFECTIVE_USERNAME, "Incoming_1")); + when(sessionService.close(uniqueId)).thenReturn(java.util.concurrent.CompletableFuture.completedFuture(null)); + when(sessionService.findByUniqueId(uniqueId)).thenReturn(java.util.concurrent.CompletableFuture.completedFuture(java.util.Optional.empty())); + + UsernameConflictResolver resolver = new UsernameConflictResolver( + conflictService, + conflictType, + sessionService, + this::messages + ); + + UsernameConflictResult result = resolver.handle( + UsernameConflictSubject.builder().build(), + "Player" + ); + + assertTrue(result.isDenied()); + assertEquals("denied", result.getDenialMessage()); + assertEquals("Incoming_1", result.getEffectiveUsername()); + verify(sessionService).close(uniqueId); + verify(sessionService).findByUniqueId(uniqueId); + } + + private AuthoritativeUsernameMessages messages() { + AuthoritativeUsernameMessages.Pipeline.Prepare prepare = new AuthoritativeUsernameMessages.Pipeline.Prepare(); + prepare.setFailed(java.util.List.of("failed")); + AuthoritativeUsernameMessages.Pipeline.Identity identity = new AuthoritativeUsernameMessages.Pipeline.Identity(); + identity.setSynchronizationFailed(java.util.List.of("sync")); + AuthoritativeUsernameMessages.Pipeline.Policy policy = new AuthoritativeUsernameMessages.Pipeline.Policy(); + policy.setPersistenceFailed(java.util.List.of("persist")); + policy.setConflictDenied(java.util.List.of("denied")); + policy.setEntrypointRequired(java.util.List.of("entrypoint")); + + AuthoritativeUsernameMessages.Pipeline pipeline = new AuthoritativeUsernameMessages.Pipeline(); + pipeline.setPrepare(prepare); + pipeline.setIdentity(identity); + pipeline.setPolicy(policy); + + AuthoritativeUsernameMessages messages = new AuthoritativeUsernameMessages(); + messages.setPipeline(pipeline); + return messages; + } +} diff --git a/identica-capability/capability-authoritative-username/authoritative-username-common/src/test/java/me/whereareiam/identica/provider/capability/authoritative/username/service/AuthoritativeUsernameConflictTypeTest.java b/identica-capability/capability-authoritative-username/authoritative-username-common/src/test/java/me/whereareiam/identica/provider/capability/authoritative/username/service/AuthoritativeUsernameConflictTypeTest.java new file mode 100644 index 000000000..7c6b35d96 --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-common/src/test/java/me/whereareiam/identica/provider/capability/authoritative/username/service/AuthoritativeUsernameConflictTypeTest.java @@ -0,0 +1,60 @@ +package me.whereareiam.identica.provider.capability.authoritative.username.service; + +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import me.whereareiam.identica.model.conflict.ConflictContext; +import me.whereareiam.identica.model.conflict.participant.ConflictParticipant; +import me.whereareiam.identica.model.conflict.participant.ConflictParticipantRole; +import me.whereareiam.identica.provider.capability.authoritative.username.UsernameConflictSchema; +import me.whereareiam.identica.provider.capability.authoritative.username.conflict.UsernameConflictType; +import me.whereareiam.identica.provider.capability.authoritative.username.conflict.UsernameEntrypointGuard; +import me.whereareiam.identica.provider.capability.authoritative.username.conflict.factory.UsernameConflictContextFactory; +import me.whereareiam.identica.provider.capability.authoritative.username.conflict.resolver.UsernameFormatResolver; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; + +@DisplayName("Authoritative Username Conflict Type") +class AuthoritativeUsernameConflictTypeTest { + @DisplayName("Provider pair matching is unordered") + @Test + void matchesProviderPairRegardlessOfOrder() { + UsernameConflictType type = new UsernameConflictType( + mock(UsernameConflictContextFactory.class), + mock(UsernameFormatResolver.class), + mock(UsernameEntrypointGuard.class) + ); + + ConflictContext context = ConflictContext.builder() + .key("username") + .hook("prepare") + .build(); + context.putParticipant(participant(UsernameConflictSchema.ROLE_INCOMING, "premium")); + context.putParticipant(participant(UsernameConflictSchema.ROLE_EXISTING, "credential")); + + com.fasterxml.jackson.databind.node.ObjectNode matching = JsonNodeFactory.instance.objectNode(); + matching.putArray("providers") + .add("credential") + .add("premium"); + assertTrue(type.matchesRule(context, matching)); + + com.fasterxml.jackson.databind.node.ObjectNode nonMatching = JsonNodeFactory.instance.objectNode(); + nonMatching.putArray("providers") + .add("premium") + .add("offline"); + assertFalse(type.matchesRule(context, nonMatching)); + } + + private ConflictParticipant participant( + ConflictParticipantRole role, + String providerId + ) { + ConflictParticipant participant = ConflictParticipant.builder() + .role(role) + .build(); + participant.putAttribute(UsernameConflictSchema.PARTICIPANT_ATTRIBUTE_PROVIDER_ID, providerId); + return participant; + } +} diff --git a/identica-capability/capability-authoritative-username/authoritative-username-database/build.gradle.kts b/identica-capability/capability-authoritative-username/authoritative-username-database/build.gradle.kts new file mode 100644 index 000000000..2e39cff77 --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-database/build.gradle.kts @@ -0,0 +1,25 @@ +plugins { + id("capability") +} + +dependencies { + compileOnly(projects.capabilityAuthoritativeUsernameApi) + testImplementation(projects.capabilityAuthoritativeUsernameApi) + testImplementation(projects.capabilityAuthoritativeUsernameCommon) + testImplementation(projects.identicaAdapterDatabase) + + compileOnly(libs.jdbi.core) + compileOnly(libs.jdbi.sqlobject) + compileOnly(libs.dialectica) + + testImplementation(libs.hikaricp) + testImplementation(libs.jdbi.core) + testImplementation(libs.jdbi.sqlobject) + testImplementation(libs.dialectica) + testImplementation(libs.h2) + testImplementation(libs.sqlite) +} + +toolkitPublish { + artifactId.set("authoritative-username-database") +} diff --git a/identica-capability/capability-authoritative-username/authoritative-username-database/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/database/AuthoritativeUsernameDatabaseModule.java b/identica-capability/capability-authoritative-username/authoritative-username-database/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/database/AuthoritativeUsernameDatabaseModule.java new file mode 100644 index 000000000..c045138cc --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-database/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/database/AuthoritativeUsernameDatabaseModule.java @@ -0,0 +1,33 @@ +package me.whereareiam.identica.provider.capability.authoritative.username.database; + +import com.google.inject.AbstractModule; +import com.google.inject.Provides; +import com.google.inject.Singleton; +import me.whereareiam.identica.database.schema.SchemaContributor; +import me.whereareiam.identica.provider.capability.authoritative.username.database.repository.AuthoritativeUsernameHistoryRepository; +import me.whereareiam.identica.provider.capability.authoritative.username.database.repository.AuthoritativeUsernameStateRepository; +import me.whereareiam.identica.provider.capability.authoritative.username.database.service.DefaultAccountUsernameHistoryPersistenceService; +import me.whereareiam.identica.provider.capability.authoritative.username.database.service.DefaultAccountUsernameStatePersistenceService; +import org.jdbi.v3.core.Jdbi; + +public class AuthoritativeUsernameDatabaseModule extends AbstractModule { + @Override + protected void configure() { + bind(AccountUsernameStatePersistenceService.class).to(DefaultAccountUsernameStatePersistenceService.class).asEagerSingleton(); + bind(AccountUsernameHistoryPersistenceService.class).to(DefaultAccountUsernameHistoryPersistenceService.class).asEagerSingleton(); + bind(AuthoritativeUsernameSchemaContributor.class).asEagerSingleton(); + bind(SchemaContributor.class).to(AuthoritativeUsernameSchemaContributor.class); + } + + @Provides + @Singleton + AuthoritativeUsernameStateRepository provideStateRepository(Jdbi jdbi) { + return jdbi.onDemand(AuthoritativeUsernameStateRepository.class); + } + + @Provides + @Singleton + AuthoritativeUsernameHistoryRepository provideHistoryRepository(Jdbi jdbi) { + return jdbi.onDemand(AuthoritativeUsernameHistoryRepository.class); + } +} diff --git a/identica-capability/capability-authoritative-username/authoritative-username-database/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/database/AuthoritativeUsernameSchemaContributor.java b/identica-capability/capability-authoritative-username/authoritative-username-database/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/database/AuthoritativeUsernameSchemaContributor.java new file mode 100644 index 000000000..cf46d0a87 --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-database/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/database/AuthoritativeUsernameSchemaContributor.java @@ -0,0 +1,16 @@ +package me.whereareiam.identica.provider.capability.authoritative.username.database; + +import me.whereareiam.dialectica.SchemaManager; +import me.whereareiam.identica.database.schema.SchemaContributor; +import org.jetbrains.annotations.NotNull; + +public final class AuthoritativeUsernameSchemaContributor implements SchemaContributor { + @Override + public void contribute(@NotNull SchemaManager schemaManager) { + ClassLoader classLoader = AuthoritativeUsernameSchemaContributor.class.getClassLoader(); + schemaManager + .scanPackages(classLoader, "me.whereareiam.identica.provider.capability.authoritative.username.database.entity") + .registerMigrationScope("authoritative-username", scope -> scope + .scanPackages(classLoader, "me.whereareiam.identica.provider.capability.authoritative.username.database.migration")); + } +} diff --git a/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/entity/UsernameHistoryEntity.java b/identica-capability/capability-authoritative-username/authoritative-username-database/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/database/entity/AuthoritativeUsernameHistoryEntity.java similarity index 54% rename from identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/entity/UsernameHistoryEntity.java rename to identica-capability/capability-authoritative-username/authoritative-username-database/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/database/entity/AuthoritativeUsernameHistoryEntity.java index ec2b5e7cb..0351ffe4c 100644 --- a/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/entity/UsernameHistoryEntity.java +++ b/identica-capability/capability-authoritative-username/authoritative-username-database/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/database/entity/AuthoritativeUsernameHistoryEntity.java @@ -1,10 +1,6 @@ -package me.whereareiam.identica.adapter.database.entity; +package me.whereareiam.identica.provider.capability.authoritative.username.database.entity; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.*; import me.whereareiam.dialectica.EntitySchemaProvider; import me.whereareiam.dialectica.annotation.Entity; @@ -15,8 +11,8 @@ @NoArgsConstructor @AllArgsConstructor @Builder(toBuilder = true) -@Entity(tableName = "identica_username_history") -public class UsernameHistoryEntity implements EntitySchemaProvider { +@Entity(tableName = "identica_account_username_history") +public class AuthoritativeUsernameHistoryEntity implements EntitySchemaProvider { private UUID uniqueId; private String providerId; private String oldUsername; @@ -28,24 +24,27 @@ public class UsernameHistoryEntity implements EntitySchemaProvider { public String statement(String databaseType) { String type = databaseType == null ? "" : databaseType.toUpperCase(); String uuidType = switch (type) { - case "POSTGRES" -> "UUID"; - case "H2" -> "UUID"; + case "POSTGRES", "H2" -> "UUID"; case "SQLITE" -> "TEXT"; default -> "CHAR(36)"; }; - String timeType = switch (type) { - case "SQLITE" -> "INTEGER"; - default -> "BIGINT"; - }; + String timeType = "SQLITE".equals(type) ? "INTEGER" : "BIGINT"; + return """ - CREATE TABLE IF NOT EXISTS identica_username_history ( + CREATE TABLE IF NOT EXISTS identica_account_username_history ( unique_id %s NOT NULL, provider_id VARCHAR(64), old_username VARCHAR(64) NOT NULL, new_username VARCHAR(64) NOT NULL, source VARCHAR(32) NOT NULL, - changed_at %s NOT NULL + changed_at %s NOT NULL, + FOREIGN KEY (unique_id) + REFERENCES identica_accounts(unique_id) + ON DELETE CASCADE ) + ; + CREATE INDEX IF NOT EXISTS identica_account_username_history_unique_id_changed_at_idx + ON identica_account_username_history (unique_id, changed_at) """.formatted(uuidType, timeType); } } diff --git a/identica-capability/capability-authoritative-username/authoritative-username-database/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/database/entity/AuthoritativeUsernameStateEntity.java b/identica-capability/capability-authoritative-username/authoritative-username-database/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/database/entity/AuthoritativeUsernameStateEntity.java new file mode 100644 index 000000000..65d474fae --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-database/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/database/entity/AuthoritativeUsernameStateEntity.java @@ -0,0 +1,42 @@ +package me.whereareiam.identica.provider.capability.authoritative.username.database.entity; + +import lombok.*; +import me.whereareiam.dialectica.EntitySchemaProvider; +import me.whereareiam.dialectica.annotation.Entity; + +import java.util.UUID; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder(toBuilder = true) +@Entity(tableName = "identica_account_username_state") +public class AuthoritativeUsernameStateEntity implements EntitySchemaProvider { + private UUID uniqueId; + private String source; + private long updatedAt; + + @Override + public String statement(String databaseType) { + String type = databaseType == null ? "" : databaseType.toUpperCase(); + String uuidType = switch (type) { + case "POSTGRES", "H2" -> "UUID"; + case "SQLITE" -> "TEXT"; + default -> "CHAR(36)"; + }; + String timeType = "SQLITE".equals(type) ? "INTEGER" : "BIGINT"; + + return """ + CREATE TABLE IF NOT EXISTS identica_account_username_state ( + unique_id %s NOT NULL, + source VARCHAR(32) NOT NULL, + updated_at %s NOT NULL, + PRIMARY KEY (unique_id), + FOREIGN KEY (unique_id) + REFERENCES identica_accounts(unique_id) + ON DELETE CASCADE + ) + """.formatted(uuidType, timeType); + } +} diff --git a/identica-capability/capability-authoritative-username/authoritative-username-database/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/database/mapper/AuthoritativeUsernameHistoryMapper.java b/identica-capability/capability-authoritative-username/authoritative-username-database/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/database/mapper/AuthoritativeUsernameHistoryMapper.java new file mode 100644 index 000000000..c8e0c0033 --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-database/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/database/mapper/AuthoritativeUsernameHistoryMapper.java @@ -0,0 +1,18 @@ +package me.whereareiam.identica.provider.capability.authoritative.username.database.mapper; + +import me.whereareiam.identica.provider.capability.authoritative.username.database.entity.AuthoritativeUsernameHistoryEntity; +import me.whereareiam.identica.provider.capability.authoritative.username.model.account.AccountUsernameHistoryEntry; + +public final class AuthoritativeUsernameHistoryMapper { + public static AuthoritativeUsernameHistoryEntity toEntity(AccountUsernameHistoryEntry entry) { + if (entry == null) return null; + return AuthoritativeUsernameHistoryEntity.builder() + .uniqueId(entry.getUniqueId()) + .providerId(entry.getProviderId()) + .oldUsername(entry.getOldUsername()) + .newUsername(entry.getNewUsername()) + .source(entry.getSource().getId()) + .changedAt(entry.getChangedAt()) + .build(); + } +} diff --git a/identica-capability/capability-authoritative-username/authoritative-username-database/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/database/mapper/AuthoritativeUsernameStateMapper.java b/identica-capability/capability-authoritative-username/authoritative-username-database/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/database/mapper/AuthoritativeUsernameStateMapper.java new file mode 100644 index 000000000..80e5efcfe --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-database/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/database/mapper/AuthoritativeUsernameStateMapper.java @@ -0,0 +1,16 @@ +package me.whereareiam.identica.provider.capability.authoritative.username.database.mapper; + +import me.whereareiam.identica.provider.capability.authoritative.username.database.entity.AuthoritativeUsernameStateEntity; +import me.whereareiam.identica.provider.capability.authoritative.username.model.account.AccountUsernameState; +import me.whereareiam.identica.provider.capability.authoritative.username.type.AccountUsernameSource; + +public final class AuthoritativeUsernameStateMapper { + public static AccountUsernameState toModel(AuthoritativeUsernameStateEntity entity) { + if (entity == null) return null; + return AccountUsernameState.builder() + .uniqueId(entity.getUniqueId()) + .source(AccountUsernameSource.fromId(entity.getSource())) + .updatedAt(entity.getUpdatedAt()) + .build(); + } +} diff --git a/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/repository/username/UsernameHistoryRepository.java b/identica-capability/capability-authoritative-username/authoritative-username-database/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/database/repository/AuthoritativeUsernameHistoryRepository.java similarity index 72% rename from identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/repository/username/UsernameHistoryRepository.java rename to identica-capability/capability-authoritative-username/authoritative-username-database/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/database/repository/AuthoritativeUsernameHistoryRepository.java index d6931a8a4..4d8890528 100644 --- a/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/repository/username/UsernameHistoryRepository.java +++ b/identica-capability/capability-authoritative-username/authoritative-username-database/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/database/repository/AuthoritativeUsernameHistoryRepository.java @@ -1,13 +1,13 @@ -package me.whereareiam.identica.adapter.database.repository.username; +package me.whereareiam.identica.provider.capability.authoritative.username.database.repository; import org.jdbi.v3.sqlobject.customizer.Bind; import org.jdbi.v3.sqlobject.statement.SqlUpdate; import java.util.UUID; -public interface UsernameHistoryRepository { +public interface AuthoritativeUsernameHistoryRepository { @SqlUpdate(""" - INSERT INTO identica_username_history ( + INSERT INTO identica_account_username_history ( unique_id, provider_id, old_username, new_username, source, changed_at ) VALUES ( :uniqueId, :providerId, :oldUsername, :newUsername, :source, :changedAt @@ -23,7 +23,7 @@ void insert( ); @SqlUpdate(""" - DELETE FROM identica_username_history + DELETE FROM identica_account_username_history WHERE unique_id = :uniqueId """) void deleteAll(@Bind("uniqueId") UUID uniqueId); diff --git a/identica-capability/capability-authoritative-username/authoritative-username-database/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/database/repository/AuthoritativeUsernameStateRepository.java b/identica-capability/capability-authoritative-username/authoritative-username-database/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/database/repository/AuthoritativeUsernameStateRepository.java new file mode 100644 index 000000000..61a99c9f6 --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-database/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/database/repository/AuthoritativeUsernameStateRepository.java @@ -0,0 +1,47 @@ +package me.whereareiam.identica.provider.capability.authoritative.username.database.repository; + +import me.whereareiam.identica.provider.capability.authoritative.username.database.entity.AuthoritativeUsernameStateEntity; +import org.jdbi.v3.sqlobject.config.RegisterBeanMapper; +import org.jdbi.v3.sqlobject.customizer.Bind; +import org.jdbi.v3.sqlobject.statement.SqlQuery; +import org.jdbi.v3.sqlobject.statement.SqlUpdate; + +import java.util.Optional; +import java.util.UUID; + +@RegisterBeanMapper(AuthoritativeUsernameStateEntity.class) +public interface AuthoritativeUsernameStateRepository { + @SqlQuery(""" + SELECT unique_id AS uniqueId, + source AS source, + updated_at AS updatedAt + FROM identica_account_username_state + WHERE unique_id = :uniqueId + """) + Optional findByUniqueId(@Bind("uniqueId") UUID uniqueId); + + @SqlUpdate(""" + INSERT INTO identica_account_username_state (unique_id, source, updated_at) + VALUES (:uniqueId, :source, :updatedAt) + """) + void insert( + @Bind("uniqueId") UUID uniqueId, + @Bind("source") String source, + @Bind("updatedAt") long updatedAt + ); + + @SqlUpdate(""" + UPDATE identica_account_username_state + SET source = :source, + updated_at = :updatedAt + WHERE unique_id = :uniqueId + """) + void update( + @Bind("uniqueId") UUID uniqueId, + @Bind("source") String source, + @Bind("updatedAt") long updatedAt + ); + + @SqlUpdate("DELETE FROM identica_account_username_state WHERE unique_id = :uniqueId") + void delete(@Bind("uniqueId") UUID uniqueId); +} diff --git a/identica-capability/capability-authoritative-username/authoritative-username-database/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/database/service/DefaultAccountUsernameHistoryPersistenceService.java b/identica-capability/capability-authoritative-username/authoritative-username-database/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/database/service/DefaultAccountUsernameHistoryPersistenceService.java new file mode 100644 index 000000000..330a2c200 --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-database/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/database/service/DefaultAccountUsernameHistoryPersistenceService.java @@ -0,0 +1,58 @@ +package me.whereareiam.identica.provider.capability.authoritative.username.database.service; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import me.whereareiam.identica.event.EventListener; +import me.whereareiam.identica.event.EventManager; +import me.whereareiam.identica.event.account.AccountLifecycleEvent; +import me.whereareiam.identica.event.base.IdenticEvent; +import me.whereareiam.identica.provider.capability.authoritative.username.database.AccountUsernameHistoryPersistenceService; +import me.whereareiam.identica.provider.capability.authoritative.username.database.mapper.AuthoritativeUsernameHistoryMapper; +import me.whereareiam.identica.provider.capability.authoritative.username.database.repository.AuthoritativeUsernameHistoryRepository; +import me.whereareiam.identica.provider.capability.authoritative.username.model.account.AccountUsernameHistoryEntry; +import me.whereareiam.identica.type.event.EventOrder; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +@Singleton +public class DefaultAccountUsernameHistoryPersistenceService implements AccountUsernameHistoryPersistenceService, EventListener { + private final AuthoritativeUsernameHistoryRepository repository; + + @Inject + public DefaultAccountUsernameHistoryPersistenceService( + @NotNull AuthoritativeUsernameHistoryRepository repository, + @NotNull EventManager eventManager + ) { + this.repository = repository; + eventManager.register(this); + } + + @Override + public void record(@NotNull AccountUsernameHistoryEntry entry) { + if (entry.getOldUsername().isBlank() || entry.getNewUsername().isBlank()) return; + + var entity = AuthoritativeUsernameHistoryMapper.toEntity(entry); + repository.insert( + entity.getUniqueId(), + entity.getProviderId(), + entity.getOldUsername(), + entity.getNewUsername(), + entity.getSource(), + entity.getChangedAt() + ); + } + + @Override + public void deleteAll(@NotNull UUID uniqueId) { + repository.deleteAll(uniqueId); + } + + @IdenticEvent(EventOrder.HIGH) + public void onAccountLifecycle(@NotNull AccountLifecycleEvent event) { + UUID uniqueId = event.getIdentity().getAccountUniqueId(); + if (uniqueId == null) return; + + deleteAll(uniqueId); + } +} diff --git a/identica-capability/capability-authoritative-username/authoritative-username-database/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/database/service/DefaultAccountUsernameStatePersistenceService.java b/identica-capability/capability-authoritative-username/authoritative-username-database/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/database/service/DefaultAccountUsernameStatePersistenceService.java new file mode 100644 index 000000000..16aa5cfeb --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-database/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/database/service/DefaultAccountUsernameStatePersistenceService.java @@ -0,0 +1,61 @@ +package me.whereareiam.identica.provider.capability.authoritative.username.database.service; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import me.whereareiam.identica.event.EventListener; +import me.whereareiam.identica.event.EventManager; +import me.whereareiam.identica.event.account.AccountLifecycleEvent; +import me.whereareiam.identica.event.base.IdenticEvent; +import me.whereareiam.identica.provider.capability.authoritative.username.database.AccountUsernameStatePersistenceService; +import me.whereareiam.identica.provider.capability.authoritative.username.database.mapper.AuthoritativeUsernameStateMapper; +import me.whereareiam.identica.provider.capability.authoritative.username.database.repository.AuthoritativeUsernameStateRepository; +import me.whereareiam.identica.provider.capability.authoritative.username.model.account.AccountUsernameState; +import me.whereareiam.identica.provider.capability.authoritative.username.type.AccountUsernameSource; +import me.whereareiam.identica.type.event.EventOrder; +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; +import java.util.UUID; + +@Singleton +public class DefaultAccountUsernameStatePersistenceService implements AccountUsernameStatePersistenceService, EventListener { + private final AuthoritativeUsernameStateRepository repository; + + @Inject + public DefaultAccountUsernameStatePersistenceService( + @NotNull AuthoritativeUsernameStateRepository repository, + @NotNull EventManager eventManager + ) { + this.repository = repository; + eventManager.register(this); + } + + @Override + public @NotNull Optional find(@NotNull UUID uniqueId) { + return repository.findByUniqueId(uniqueId).map(AuthoritativeUsernameStateMapper::toModel); + } + + @Override + public void save(@NotNull UUID uniqueId, @NotNull AccountUsernameSource source) { + long now = System.currentTimeMillis(); + if (repository.findByUniqueId(uniqueId).isPresent()) { + repository.update(uniqueId, source.getId(), now); + return; + } + + repository.insert(uniqueId, source.getId(), now); + } + + @Override + public void delete(@NotNull UUID uniqueId) { + repository.delete(uniqueId); + } + + @IdenticEvent(EventOrder.HIGH) + public void onAccountLifecycle(@NotNull AccountLifecycleEvent event) { + UUID uniqueId = event.getIdentity().getAccountUniqueId(); + if (uniqueId == null) return; + + delete(uniqueId); + } +} diff --git a/identica-capability/capability-authoritative-username/authoritative-username-database/src/test/java/me/whereareiam/identica/provider/capability/authoritative/username/database/AuthoritativeUsernameDatabaseModuleTest.java b/identica-capability/capability-authoritative-username/authoritative-username-database/src/test/java/me/whereareiam/identica/provider/capability/authoritative/username/database/AuthoritativeUsernameDatabaseModuleTest.java new file mode 100644 index 000000000..f88c05050 --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username-database/src/test/java/me/whereareiam/identica/provider/capability/authoritative/username/database/AuthoritativeUsernameDatabaseModuleTest.java @@ -0,0 +1,49 @@ +package me.whereareiam.identica.provider.capability.authoritative.username.database; + +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import me.whereareiam.identica.event.EventManager; +import me.whereareiam.identica.provider.capability.authoritative.username.database.repository.AuthoritativeUsernameHistoryRepository; +import me.whereareiam.identica.provider.capability.authoritative.username.database.repository.AuthoritativeUsernameStateRepository; +import me.whereareiam.identica.provider.capability.authoritative.username.database.service.DefaultAccountUsernameHistoryPersistenceService; +import me.whereareiam.identica.provider.capability.authoritative.username.database.service.DefaultAccountUsernameStatePersistenceService; +import org.jdbi.v3.core.Jdbi; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@DisplayName("Authoritative Username Database Module") +class AuthoritativeUsernameDatabaseModuleTest { + @Test + void bindsPersistenceServices() { + Jdbi jdbi = mock(Jdbi.class); + AuthoritativeUsernameStateRepository stateRepository = mock(AuthoritativeUsernameStateRepository.class); + AuthoritativeUsernameHistoryRepository historyRepository = mock(AuthoritativeUsernameHistoryRepository.class); + when(jdbi.onDemand(AuthoritativeUsernameStateRepository.class)).thenReturn(stateRepository); + when(jdbi.onDemand(AuthoritativeUsernameHistoryRepository.class)).thenReturn(historyRepository); + + Injector injector = Guice.createInjector( + new AuthoritativeUsernameDatabaseModule(), + new AbstractModule() { + @Override + protected void configure() { + bind(Jdbi.class).toInstance(jdbi); + bind(EventManager.class).toInstance(mock(EventManager.class)); + } + } + ); + + assertInstanceOf( + DefaultAccountUsernameStatePersistenceService.class, + injector.getInstance(AccountUsernameStatePersistenceService.class) + ); + assertInstanceOf( + DefaultAccountUsernameHistoryPersistenceService.class, + injector.getInstance(AccountUsernameHistoryPersistenceService.class) + ); + } +} diff --git a/identica-capability/capability-authoritative-username/authoritative-username/build.gradle.kts b/identica-capability/capability-authoritative-username/authoritative-username/build.gradle.kts new file mode 100644 index 000000000..93434498a --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("capability-artifact") +} + +dependencies { + api(projects.capabilityAuthoritativeUsernameApi) + implementation(projects.capabilityAuthoritativeUsernameCommon) + implementation(projects.capabilityAuthoritativeUsernameDatabase) +} + +toolkitPublish { + artifactId.set("authoritative-username") +} diff --git a/identica-capability/capability-authoritative-username/authoritative-username/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/bootstrap/AuthoritativeUsernameCapabilityBootstrap.java b/identica-capability/capability-authoritative-username/authoritative-username/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/bootstrap/AuthoritativeUsernameCapabilityBootstrap.java new file mode 100644 index 000000000..423acc33c --- /dev/null +++ b/identica-capability/capability-authoritative-username/authoritative-username/src/main/java/me/whereareiam/identica/provider/capability/authoritative/username/bootstrap/AuthoritativeUsernameCapabilityBootstrap.java @@ -0,0 +1,64 @@ +package me.whereareiam.identica.provider.capability.authoritative.username.bootstrap; + +import com.google.inject.Injector; +import com.google.inject.Module; +import me.whereareiam.identica.conflict.ConflictService; +import me.whereareiam.identica.database.schema.SchemaBootstrap; +import me.whereareiam.identica.model.provider.capability.ProviderCapabilityDeclaration; +import me.whereareiam.identica.pipeline.extension.PipelineExtensionRegistry; +import me.whereareiam.identica.provider.capability.authoritative.username.AuthoritativeUsernameGlobalModule; +import me.whereareiam.identica.provider.capability.authoritative.username.conflict.UsernameConflictType; +import me.whereareiam.identica.provider.capability.authoritative.username.database.AuthoritativeUsernameDatabaseModule; +import me.whereareiam.identica.provider.capability.authoritative.username.database.AuthoritativeUsernameSchemaContributor; +import me.whereareiam.identica.provider.capability.authoritative.username.pipeline.AuthoritativeUsernamePipelineExtension; +import me.whereareiam.identica.provider.capability.authoritative.username.type.AuthoritativeUsernameCapability; +import me.whereareiam.identica.provider.capability.bootstrap.ProviderCapabilityBootstrap; +import me.whereareiam.identica.provider.capability.bootstrap.ProviderCapabilityGlobalInstallContext; +import me.whereareiam.identica.provider.capability.bootstrap.ProviderCapabilityInitializationContext; +import me.whereareiam.identica.type.provider.capability.ProviderCapabilityScope; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Set; + +/** + * Bootstrap for the built-in authoritative username capability. + */ +public final class AuthoritativeUsernameCapabilityBootstrap implements ProviderCapabilityBootstrap { + public static final @NotNull AuthoritativeUsernameCapabilityBootstrap INSTANCE = + new AuthoritativeUsernameCapabilityBootstrap(); + + @Override + public @NotNull ProviderCapabilityDeclaration declaration() { + return ProviderCapabilityDeclaration.builder() + .capability(AuthoritativeUsernameCapability.CAPABILITY) + .scopes(Set.of(ProviderCapabilityScope.GLOBAL, ProviderCapabilityScope.LOCAL)) + .build(); + } + + @Override + public void initialize(@NotNull ProviderCapabilityInitializationContext context) { + Injector globalInjector = context.getGlobalInjector(); + if (globalInjector == null) return; + + context.getRootInjector() + .getInstance(PipelineExtensionRegistry.class) + .register(globalInjector.getInstance(AuthoritativeUsernamePipelineExtension.class)); + context.getRootInjector() + .getInstance(ConflictService.class) + .register(globalInjector.getInstance(UsernameConflictType.class)); + context.getRootInjector() + .getInstance(SchemaBootstrap.class) + .apply(globalInjector.getInstance(AuthoritativeUsernameSchemaContributor.class)); + } + + @Override + public @NotNull List globalModules(@NotNull ProviderCapabilityGlobalInstallContext context) { + return List.of( + new AuthoritativeUsernameGlobalModule( + context.getCapabilitiesPath().resolve(AuthoritativeUsernameCapability.CAPABILITY.getId()) + ), + new AuthoritativeUsernameDatabaseModule() + ); + } +} diff --git a/identica-capability/capability-authoritative-username/build.gradle.kts b/identica-capability/capability-authoritative-username/build.gradle.kts new file mode 100644 index 000000000..6c8344122 --- /dev/null +++ b/identica-capability/capability-authoritative-username/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + id("base") +} diff --git a/identica-capability/capability-recognition/build.gradle.kts b/identica-capability/capability-recognition/build.gradle.kts new file mode 100644 index 000000000..6c8344122 --- /dev/null +++ b/identica-capability/capability-recognition/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + id("base") +} diff --git a/identica-capability/capability-recognition/recognition-api/build.gradle.kts b/identica-capability/capability-recognition/recognition-api/build.gradle.kts new file mode 100644 index 000000000..4bae45e73 --- /dev/null +++ b/identica-capability/capability-recognition/recognition-api/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + id("capability") +} + +toolkitPublish { + artifactId.set("recognition-api") +} diff --git a/identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/RecognitionCapability.java b/identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/RecognitionCapability.java new file mode 100644 index 000000000..001f53ae5 --- /dev/null +++ b/identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/RecognitionCapability.java @@ -0,0 +1,11 @@ +package me.whereareiam.identica.provider.capability.recognition; + +import me.whereareiam.identica.type.provider.capability.ProviderCapability; +import org.jetbrains.annotations.NotNull; + +/** + * Built-in reconnect recognition capability identifier. + */ +public final class RecognitionCapability { + public static final @NotNull ProviderCapability CAPABILITY = ProviderCapability.of("recognition"); +} diff --git a/identica-api/src/main/java/me/whereareiam/identica/identity/session/recognition/SessionRecognitionService.java b/identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/SessionRecognitionService.java similarity index 93% rename from identica-api/src/main/java/me/whereareiam/identica/identity/session/recognition/SessionRecognitionService.java rename to identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/SessionRecognitionService.java index 865009e98..e7dd5f879 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/identity/session/recognition/SessionRecognitionService.java +++ b/identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/SessionRecognitionService.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.identity.session.recognition; +package me.whereareiam.identica.provider.capability.recognition; import me.whereareiam.identica.identity.actor.ConnectionIdentity; import org.jetbrains.annotations.Nullable; diff --git a/identica-api/src/main/java/me/whereareiam/identica/identity/session/recognition/eligibility/RecognitionEligibilityRegistry.java b/identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/eligibility/RecognitionEligibilityRegistry.java similarity index 92% rename from identica-api/src/main/java/me/whereareiam/identica/identity/session/recognition/eligibility/RecognitionEligibilityRegistry.java rename to identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/eligibility/RecognitionEligibilityRegistry.java index f4064fde7..56ce9508b 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/identity/session/recognition/eligibility/RecognitionEligibilityRegistry.java +++ b/identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/eligibility/RecognitionEligibilityRegistry.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.identity.session.recognition.eligibility; +package me.whereareiam.identica.provider.capability.recognition.eligibility; import org.jetbrains.annotations.NotNull; diff --git a/identica-api/src/main/java/me/whereareiam/identica/identity/session/recognition/eligibility/RecognitionEligibilityRule.java b/identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/eligibility/RecognitionEligibilityRule.java similarity index 82% rename from identica-api/src/main/java/me/whereareiam/identica/identity/session/recognition/eligibility/RecognitionEligibilityRule.java rename to identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/eligibility/RecognitionEligibilityRule.java index b1dd7adda..bb74b06b3 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/identity/session/recognition/eligibility/RecognitionEligibilityRule.java +++ b/identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/eligibility/RecognitionEligibilityRule.java @@ -1,7 +1,7 @@ -package me.whereareiam.identica.identity.session.recognition.eligibility; +package me.whereareiam.identica.provider.capability.recognition.eligibility; -import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityContext; -import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityRuleDecision; +import me.whereareiam.identica.provider.capability.recognition.model.eligibility.RecognitionEligibilityContext; +import me.whereareiam.identica.provider.capability.recognition.model.eligibility.RecognitionEligibilityRuleDecision; import org.jetbrains.annotations.NotNull; /** diff --git a/identica-api/src/main/java/me/whereareiam/identica/identity/session/recognition/eligibility/RecognitionEligibilityService.java b/identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/eligibility/RecognitionEligibilityService.java similarity index 78% rename from identica-api/src/main/java/me/whereareiam/identica/identity/session/recognition/eligibility/RecognitionEligibilityService.java rename to identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/eligibility/RecognitionEligibilityService.java index 72c4cfc47..e2c51cf40 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/identity/session/recognition/eligibility/RecognitionEligibilityService.java +++ b/identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/eligibility/RecognitionEligibilityService.java @@ -1,7 +1,7 @@ -package me.whereareiam.identica.identity.session.recognition.eligibility; +package me.whereareiam.identica.provider.capability.recognition.eligibility; -import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityContext; -import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityDecision; +import me.whereareiam.identica.provider.capability.recognition.model.eligibility.RecognitionEligibilityContext; +import me.whereareiam.identica.provider.capability.recognition.model.eligibility.RecognitionEligibilityDecision; import org.jetbrains.annotations.NotNull; /** diff --git a/identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/event/RecognitionAppliedEvent.java b/identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/event/RecognitionAppliedEvent.java new file mode 100644 index 000000000..49c0a9067 --- /dev/null +++ b/identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/event/RecognitionAppliedEvent.java @@ -0,0 +1,25 @@ +package me.whereareiam.identica.provider.capability.recognition.event; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; +import me.whereareiam.identica.event.base.Event; +import me.whereareiam.identica.event.base.SynchronousEvent; +import me.whereareiam.identica.model.Session; +import me.whereareiam.identica.type.pipeline.PipelineType; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** + * Event fired when a session opens for an authentication that completed via + * reconnect recognition. + */ +@Getter +@ToString +@RequiredArgsConstructor +public class RecognitionAppliedEvent implements Event, SynchronousEvent { + private final @NotNull UUID connectionUniqueId; + private final @NotNull PipelineType pipelineType; + private final @NotNull Session session; +} diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/session/SessionRecognitionSnapshot.java b/identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/model/SessionRecognitionSnapshot.java similarity index 90% rename from identica-api/src/main/java/me/whereareiam/identica/model/session/SessionRecognitionSnapshot.java rename to identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/model/SessionRecognitionSnapshot.java index ac7a85546..ada022b6d 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/session/SessionRecognitionSnapshot.java +++ b/identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/model/SessionRecognitionSnapshot.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.model.session; +package me.whereareiam.identica.provider.capability.recognition.model; import lombok.*; import org.jetbrains.annotations.NotNull; diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/session/recognition/eligibility/RecognitionEligibilityContext.java b/identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/model/eligibility/RecognitionEligibilityContext.java similarity index 75% rename from identica-api/src/main/java/me/whereareiam/identica/model/session/recognition/eligibility/RecognitionEligibilityContext.java rename to identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/model/eligibility/RecognitionEligibilityContext.java index 9c08a03fc..32b28b6ac 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/session/recognition/eligibility/RecognitionEligibilityContext.java +++ b/identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/model/eligibility/RecognitionEligibilityContext.java @@ -1,12 +1,12 @@ -package me.whereareiam.identica.model.session.recognition.eligibility; +package me.whereareiam.identica.provider.capability.recognition.model.eligibility; import lombok.Builder; import lombok.Getter; import lombok.ToString; import me.whereareiam.identica.identity.actor.ConnectionIdentity; import me.whereareiam.identica.model.provider.ProviderContext; -import me.whereareiam.identica.type.session.recognition.RecognitionAttemptKind; -import me.whereareiam.identica.type.session.recognition.RecognitionTrigger; +import me.whereareiam.identica.provider.capability.recognition.type.RecognitionAttemptKind; +import me.whereareiam.identica.provider.capability.recognition.type.RecognitionTrigger; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/session/recognition/eligibility/RecognitionEligibilityDecision.java b/identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/model/eligibility/RecognitionEligibilityDecision.java similarity index 82% rename from identica-api/src/main/java/me/whereareiam/identica/model/session/recognition/eligibility/RecognitionEligibilityDecision.java rename to identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/model/eligibility/RecognitionEligibilityDecision.java index 179f7e70d..406caf964 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/session/recognition/eligibility/RecognitionEligibilityDecision.java +++ b/identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/model/eligibility/RecognitionEligibilityDecision.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.model.session.recognition.eligibility; +package me.whereareiam.identica.provider.capability.recognition.model.eligibility; import lombok.Builder; import lombok.Getter; diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/session/recognition/eligibility/RecognitionEligibilityRuleDecision.java b/identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/model/eligibility/RecognitionEligibilityRuleDecision.java similarity index 94% rename from identica-api/src/main/java/me/whereareiam/identica/model/session/recognition/eligibility/RecognitionEligibilityRuleDecision.java rename to identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/model/eligibility/RecognitionEligibilityRuleDecision.java index 4e2682b27..78388a36f 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/session/recognition/eligibility/RecognitionEligibilityRuleDecision.java +++ b/identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/model/eligibility/RecognitionEligibilityRuleDecision.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.model.session.recognition.eligibility; +package me.whereareiam.identica.provider.capability.recognition.model.eligibility; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/store/RecognizedConnectionStore.java b/identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/store/RecognizedConnectionStore.java new file mode 100644 index 000000000..a91c9864d --- /dev/null +++ b/identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/store/RecognizedConnectionStore.java @@ -0,0 +1,36 @@ +package me.whereareiam.identica.provider.capability.recognition.store; + +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** + * Stores whether a live authentication connection completed via recognition. + * + *

The state is scoped to a single connection attempt. A reconnect receives a + * new connection UUID and must not inherit an earlier recognized outcome.

+ */ +public interface RecognizedConnectionStore { + /** + * Marks the supplied connection as recognized. + * + * @param connectionUniqueId recognized connection id + */ + void markRecognized(@NotNull UUID connectionUniqueId); + + /** + * Returns whether the supplied connection is currently marked recognized. + * + * @param connectionUniqueId connection id to check + * @return {@code true} when the connection is marked recognized + */ + boolean isRecognized(@NotNull UUID connectionUniqueId); + + /** + * Consumes and clears the recognized state for a connection. + * + * @param connectionUniqueId connection id to consume + * @return {@code true} when a recognized state was present + */ + boolean consumeRecognized(@NotNull UUID connectionUniqueId); +} diff --git a/identica-api/src/main/java/me/whereareiam/identica/identity/session/recognition/SessionRecognitionStore.java b/identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/store/SessionRecognitionStore.java similarity index 86% rename from identica-api/src/main/java/me/whereareiam/identica/identity/session/recognition/SessionRecognitionStore.java rename to identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/store/SessionRecognitionStore.java index 994637306..e63ef9a08 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/identity/session/recognition/SessionRecognitionStore.java +++ b/identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/store/SessionRecognitionStore.java @@ -1,6 +1,6 @@ -package me.whereareiam.identica.identity.session.recognition; +package me.whereareiam.identica.provider.capability.recognition.store; -import me.whereareiam.identica.model.session.SessionRecognitionSnapshot; +import me.whereareiam.identica.provider.capability.recognition.model.SessionRecognitionSnapshot; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-api/src/main/java/me/whereareiam/identica/type/session/recognition/RecognitionAttemptKind.java b/identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/type/RecognitionAttemptKind.java similarity index 69% rename from identica-api/src/main/java/me/whereareiam/identica/type/session/recognition/RecognitionAttemptKind.java rename to identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/type/RecognitionAttemptKind.java index 5be227912..80b9ff262 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/type/session/recognition/RecognitionAttemptKind.java +++ b/identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/type/RecognitionAttemptKind.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.type.session.recognition; +package me.whereareiam.identica.provider.capability.recognition.type; /** * Recognition attempt category that is being evaluated. diff --git a/identica-api/src/main/java/me/whereareiam/identica/type/session/recognition/RecognitionSignal.java b/identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/type/RecognitionSignal.java similarity index 67% rename from identica-api/src/main/java/me/whereareiam/identica/type/session/recognition/RecognitionSignal.java rename to identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/type/RecognitionSignal.java index 3a9385159..7074adf35 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/type/session/recognition/RecognitionSignal.java +++ b/identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/type/RecognitionSignal.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.type.session.recognition; +package me.whereareiam.identica.provider.capability.recognition.type; /** * Signals that may be matched before a reconnecting player is recognized. diff --git a/identica-api/src/main/java/me/whereareiam/identica/type/session/recognition/RecognitionTrigger.java b/identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/type/RecognitionTrigger.java similarity index 67% rename from identica-api/src/main/java/me/whereareiam/identica/type/session/recognition/RecognitionTrigger.java rename to identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/type/RecognitionTrigger.java index 971964f3f..84da34550 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/type/session/recognition/RecognitionTrigger.java +++ b/identica-capability/capability-recognition/recognition-api/src/main/java/me/whereareiam/identica/provider/capability/recognition/type/RecognitionTrigger.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.type.session.recognition; +package me.whereareiam.identica.provider.capability.recognition.type; /** * Trigger that caused a recognition attempt to be evaluated. diff --git a/identica-capability/capability-recognition/recognition-common/build.gradle.kts b/identica-capability/capability-recognition/recognition-common/build.gradle.kts new file mode 100644 index 000000000..5daf95496 --- /dev/null +++ b/identica-capability/capability-recognition/recognition-common/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("capability") +} + +dependencies { + api(projects.capabilityRecognitionApi) + api(projects.capabilityRestrictionApi) + api(projects.capabilityRestrictionJoinApi) +} + +toolkitPublish { + artifactId.set("recognition-common") +} diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/DefaultSessionRecognitionService.java b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/DefaultSessionRecognitionService.java similarity index 61% rename from identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/DefaultSessionRecognitionService.java rename to identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/DefaultSessionRecognitionService.java index 36cced0ba..abb014a5a 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/DefaultSessionRecognitionService.java +++ b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/DefaultSessionRecognitionService.java @@ -1,20 +1,21 @@ -package me.whereareiam.identica.common.identity.session.recognition; +package me.whereareiam.identica.provider.capability.recognition; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; import me.whereareiam.identica.identity.actor.ConnectionIdentity; -import me.whereareiam.identica.identity.session.recognition.SessionRecognitionService; -import me.whereareiam.identica.identity.session.recognition.SessionRecognitionStore; -import me.whereareiam.identica.identity.session.recognition.eligibility.RecognitionEligibilityService; import me.whereareiam.identica.model.config.provider.Providers; -import me.whereareiam.identica.model.config.Settings; -import me.whereareiam.identica.model.session.SessionRecognitionSnapshot; -import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityContext; -import me.whereareiam.identica.type.session.recognition.RecognitionAttemptKind; -import me.whereareiam.identica.type.session.recognition.RecognitionSignal; -import me.whereareiam.identica.type.session.recognition.RecognitionTrigger; +import me.whereareiam.identica.provider.capability.recognition.config.RecognitionSettings; +import me.whereareiam.identica.provider.capability.recognition.config.provider.RecognitionCapabilities; +import me.whereareiam.identica.provider.capability.recognition.config.provider.RecognitionProvidersProvider; +import me.whereareiam.identica.provider.capability.recognition.eligibility.RecognitionEligibilityService; +import me.whereareiam.identica.provider.capability.recognition.model.SessionRecognitionSnapshot; +import me.whereareiam.identica.provider.capability.recognition.model.eligibility.RecognitionEligibilityContext; +import me.whereareiam.identica.provider.capability.recognition.store.SessionRecognitionStore; +import me.whereareiam.identica.provider.capability.recognition.type.RecognitionAttemptKind; +import me.whereareiam.identica.provider.capability.recognition.type.RecognitionSignal; +import me.whereareiam.identica.provider.capability.recognition.type.RecognitionTrigger; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -26,8 +27,8 @@ @Singleton @RequiredArgsConstructor(onConstructor_ = @Inject) public class DefaultSessionRecognitionService implements SessionRecognitionService { - private final Provider settingsProvider; - private final Provider providersProvider; + private final Provider settingsProvider; + private final RecognitionProvidersProvider providersProvider; private final SessionRecognitionStore sessionRecognitionStore; private final RecognitionEligibilityService recognitionEligibilityService; @@ -39,8 +40,7 @@ public boolean matches( @Nullable String ip, @Nullable ConnectionIdentity.Origin origin ) { - if (isBlank(providerId) || isBlank(providerSubject) || !isRecognitionEnabled(providerId)) - return false; + if (isBlank(providerId) || isBlank(providerSubject) || !isRecognitionEnabled(providerId)) return false; if (!recognitionEligibilityService.evaluate(RecognitionEligibilityContext.builder() .providerId(providerId) .providerUsername(providerUsername) @@ -87,54 +87,43 @@ private boolean matchesOrigin( } private boolean isRecognitionEnabled(@NotNull String providerId) { - Providers.ProviderEntry provider = findProvider(providerId); - Providers.ProviderEntry.Session session = provider != null - ? provider.getSession() - : null; - Providers.ProviderEntry.Session.Recognition recognition = session != null - ? session.getRecognition() - : null; - - Boolean override = recognition != null ? recognition.getEnabled() : null; + RecognitionCapabilities.Recognition provider = findProviderRecognition(providerId); + Boolean override = provider != null ? provider.getEnabled() : null; if (override != null) return override; - return settingsProvider.get().getSessions().getRecognition().isEnabled(); + return settingsProvider.get().isEnabled(); } private @NotNull Set effectiveSignals(@NotNull String providerId) { LinkedHashSet resolved = new LinkedHashSet<>(); - Providers.ProviderEntry provider = findProvider(providerId); - Providers.ProviderEntry.Session session = provider != null - ? provider.getSession() - : null; - Providers.ProviderEntry.Session.Recognition recognition = session != null - ? session.getRecognition() - : null; - List signals = recognition != null - ? recognition.getSignals() - : List.of(); + RecognitionCapabilities.Recognition provider = findProviderRecognition(providerId); + List signals = provider != null ? provider.getSignals() : List.of(); if (!signals.isEmpty()) { resolved.addAll(signals); return resolved; } - List defaults = settingsProvider.get() - .getSessions() - .getRecognition() - .getDefaultSignals(); + List defaults = settingsProvider.get().getDefaultSignals(); - resolved.addAll(defaults); + resolved.addAll(defaults); return resolved; } - private @Nullable Providers.ProviderEntry findProvider(@Nullable String rawId) { + private @Nullable RecognitionCapabilities.Recognition findProviderRecognition(@Nullable String rawId) { if (isBlank(rawId)) return null; for (Providers.ProviderEntry provider : providersProvider.get().getProviders()) { - if (provider == null || isBlank(provider.getId())) continue; - if (provider.getId().trim().equalsIgnoreCase(rawId.trim())) return provider; + if (provider == null || provider.getId().isBlank()) continue; + if (!provider.getId().trim().equalsIgnoreCase(rawId.trim())) continue; + + Providers.ProviderEntry.Capabilities capabilities = provider.getCapabilities(); + if (!(capabilities instanceof RecognitionCapabilities recognitionCapabilities)) + return null; + + return recognitionCapabilities.getRecognition(); } + return null; } diff --git a/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/RecognitionGlobalModule.java b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/RecognitionGlobalModule.java new file mode 100644 index 000000000..76b29d434 --- /dev/null +++ b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/RecognitionGlobalModule.java @@ -0,0 +1,53 @@ +package me.whereareiam.identica.provider.capability.recognition; + +import com.google.inject.AbstractModule; +import com.google.inject.Provides; +import com.google.inject.Singleton; +import com.google.inject.name.Named; +import lombok.RequiredArgsConstructor; +import me.whereareiam.identica.provider.capability.recognition.config.RecognitionSettings; +import me.whereareiam.identica.provider.capability.recognition.config.provider.RecognitionProvidersProvider; +import me.whereareiam.identica.provider.capability.recognition.config.provider.RecognitionSettingsProvider; +import me.whereareiam.identica.provider.capability.recognition.eligibility.DefaultRecognitionEligibilityRegistry; +import me.whereareiam.identica.provider.capability.recognition.eligibility.DefaultRecognitionEligibilityService; +import me.whereareiam.identica.provider.capability.recognition.eligibility.RecognitionEligibilityRegistry; +import me.whereareiam.identica.provider.capability.recognition.eligibility.RecognitionEligibilityService; +import me.whereareiam.identica.provider.capability.recognition.pipeline.RecognitionAppliedLifecycle; +import me.whereareiam.identica.provider.capability.recognition.pipeline.RecognitionPipelineExtension; +import me.whereareiam.identica.provider.capability.recognition.store.DefaultRecognizedConnectionStore; +import me.whereareiam.identica.provider.capability.recognition.store.DefaultSessionRecognitionStore; +import me.whereareiam.identica.provider.capability.recognition.store.RecognizedConnectionStore; +import me.whereareiam.identica.provider.capability.recognition.store.SessionRecognitionStore; + +import java.nio.file.Path; + +@RequiredArgsConstructor +public class RecognitionGlobalModule extends AbstractModule { + private final Path recognitionCapabilityPath; + + @Override + protected void configure() { + // Configuration + bind(RecognitionSettingsProvider.class).asEagerSingleton(); + bind(RecognitionSettings.class).toProvider(RecognitionSettingsProvider.class); + bind(RecognitionProvidersProvider.class).asEagerSingleton(); + + // Services + bind(SessionRecognitionStore.class).to(DefaultSessionRecognitionStore.class).asEagerSingleton(); + bind(SessionRecognitionService.class).to(DefaultSessionRecognitionService.class).asEagerSingleton(); + bind(RecognizedConnectionStore.class).to(DefaultRecognizedConnectionStore.class).asEagerSingleton(); + bind(RecognitionEligibilityRegistry.class).to(DefaultRecognitionEligibilityRegistry.class).asEagerSingleton(); + bind(RecognitionEligibilityService.class).to(DefaultRecognitionEligibilityService.class).asEagerSingleton(); + + // Pipeline + bind(RecognitionPipelineExtension.class).asEagerSingleton(); + bind(RecognitionAppliedLifecycle.class).asEagerSingleton(); + } + + @Provides + @Singleton + @Named("recognitionCapabilityPath") + Path provideRecognitionCapabilityPath() { + return recognitionCapabilityPath; + } +} diff --git a/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/RecognitionLocalModule.java b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/RecognitionLocalModule.java new file mode 100644 index 000000000..3ed7aaa37 --- /dev/null +++ b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/RecognitionLocalModule.java @@ -0,0 +1,15 @@ +package me.whereareiam.identica.provider.capability.recognition; + +import com.google.inject.AbstractModule; +import com.google.inject.multibindings.Multibinder; +import me.whereareiam.identica.provider.capability.contribution.ProviderCapabilityContribution; +import me.whereareiam.identica.provider.capability.recognition.compatibility.restriction.join.RecognitionJoinSignalContribution; + +public class RecognitionLocalModule extends AbstractModule { + @Override + protected void configure() { + Multibinder.newSetBinder(binder(), ProviderCapabilityContribution.class) + .addBinding() + .to(RecognitionJoinSignalContribution.class); + } +} diff --git a/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/compatibility/restriction/join/RecognitionJoinSignalContribution.java b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/compatibility/restriction/join/RecognitionJoinSignalContribution.java new file mode 100644 index 000000000..9d839c7bf --- /dev/null +++ b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/compatibility/restriction/join/RecognitionJoinSignalContribution.java @@ -0,0 +1,56 @@ +package me.whereareiam.identica.provider.capability.recognition.compatibility.restriction.join; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import me.whereareiam.identica.provider.capability.recognition.RecognitionCapability; +import me.whereareiam.identica.provider.capability.recognition.SessionRecognitionService; +import me.whereareiam.identica.provider.capability.restriction.contribution.ProviderRestrictionSignalContribution; +import me.whereareiam.identica.provider.capability.restriction.join.JoinRestrictionType; +import me.whereareiam.identica.provider.capability.restriction.model.RestrictionEvaluationRequest; +import me.whereareiam.identica.provider.capability.restriction.type.RestrictionSignal; +import me.whereareiam.identica.provider.capability.restriction.type.RestrictionType; +import me.whereareiam.identica.type.provider.capability.ProviderCapability; +import org.jetbrains.annotations.NotNull; + +@Singleton +public class RecognitionJoinSignalContribution implements ProviderRestrictionSignalContribution { + private static final RestrictionSignal RECOGNIZED = RestrictionSignal.of("recognized"); + + private final SessionRecognitionService sessionRecognitionService; + + @Inject + public RecognitionJoinSignalContribution(SessionRecognitionService sessionRecognitionService) { + this.sessionRecognitionService = sessionRecognitionService; + } + + @Override + public @NotNull ProviderCapability capability() { + return RecognitionCapability.CAPABILITY; + } + + @Override + public @NotNull RestrictionType restrictionType() { + return JoinRestrictionType.TYPE; + } + + @Override + public @NotNull RestrictionSignal signal() { + return RECOGNIZED; + } + + @Override + public boolean matches(@NotNull RestrictionEvaluationRequest request) { + String providerId = request.getProviderId(); + String providerSubject = request.getProviderSubject(); + if (providerId == null || providerId.isBlank()) return false; + if (providerSubject == null || providerSubject.isBlank()) return false; + + return sessionRecognitionService.matches( + providerId, + providerSubject, + request.getProviderUsername(), + request.getIp(), + request.getOrigin() + ); + } +} diff --git a/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/config/RecognitionConfigBindings.java b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/config/RecognitionConfigBindings.java new file mode 100644 index 000000000..c23400dab --- /dev/null +++ b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/config/RecognitionConfigBindings.java @@ -0,0 +1,9 @@ +package me.whereareiam.identica.provider.capability.recognition.config; + +import com.google.inject.AbstractModule; + +public class RecognitionConfigBindings extends AbstractModule { + @Override + protected void configure() { + } +} diff --git a/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/config/RecognitionSettings.java b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/config/RecognitionSettings.java new file mode 100644 index 000000000..30f8870ea --- /dev/null +++ b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/config/RecognitionSettings.java @@ -0,0 +1,60 @@ +package me.whereareiam.identica.provider.capability.recognition.config; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import me.whereareiam.configura.ConfigDocument; +import me.whereareiam.identica.provider.capability.recognition.type.RecognitionSignal; +import org.jetbrains.annotations.NotNull; + +import java.time.Duration; +import java.util.List; + +@Getter +@Setter +@ToString +public class RecognitionSettings extends ConfigDocument { + private boolean enabled; + private @NotNull Duration validity; + private @NotNull Duration window; + private @NotNull List defaultSignals; + private @NotNull Eligibility eligibility; + private @NotNull Replication replication; + + public long validityMillis() { + if (validity.isZero() || validity.isNegative()) + throw new IllegalStateException("providers.capabilities.recognition.settings.validity must be positive"); + + return validity.toMillis(); + } + + public long windowMillis() { + if (window.isZero() || window.isNegative()) + throw new IllegalStateException("providers.capabilities.recognition.settings.window must be positive"); + + return window.toMillis(); + } + + @Getter + @Setter + @ToString + public static class Eligibility { + private @NotNull UntrustedIps untrustedIps; + + @Getter + @Setter + @ToString + public static class UntrustedIps { + private boolean enabled; + private @NotNull List entries; + } + } + + @Getter + @Setter + @ToString + public static class Replication { + private @NotNull String snapshotNamespace; + private @NotNull String recognizedConnectionNamespace; + } +} diff --git a/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/config/defaults/RecognitionSettingsDefaults.java b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/config/defaults/RecognitionSettingsDefaults.java new file mode 100644 index 000000000..dd719795f --- /dev/null +++ b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/config/defaults/RecognitionSettingsDefaults.java @@ -0,0 +1,43 @@ +package me.whereareiam.identica.provider.capability.recognition.config.defaults; + +import com.google.inject.Singleton; +import me.whereareiam.configura.merge.defaults.DefaultsProvider; +import me.whereareiam.identica.provider.capability.recognition.config.RecognitionSettings; +import me.whereareiam.identica.provider.capability.recognition.type.RecognitionSignal; + +import java.time.Duration; +import java.util.List; + +@Singleton +public class RecognitionSettingsDefaults implements DefaultsProvider { + @Override + public RecognitionSettings supply(RecognitionSettings settings) { + settings.setEnabled(false); + settings.setValidity(Duration.ofHours(12)); + settings.setWindow(Duration.ofMinutes(10)); + settings.setDefaultSignals(List.of( + RecognitionSignal.USERNAME, + RecognitionSignal.IP, + RecognitionSignal.VIRTUAL_HOST + )); + + RecognitionSettings.Eligibility.UntrustedIps untrustedIps = new RecognitionSettings.Eligibility.UntrustedIps(); + untrustedIps.setEnabled(true); + untrustedIps.setEntries(List.of( + "127.0.0.1", + "::1", + "10.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16" + )); + RecognitionSettings.Eligibility eligibility = new RecognitionSettings.Eligibility(); + eligibility.setUntrustedIps(untrustedIps); + settings.setEligibility(eligibility); + + RecognitionSettings.Replication replication = new RecognitionSettings.Replication(); + replication.setSnapshotNamespace("identica:session-recognition:snapshot"); + replication.setRecognizedConnectionNamespace("identica:recognized-connection"); + settings.setReplication(replication); + return settings; + } +} diff --git a/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/config/provider/RecognitionCapabilities.java b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/config/provider/RecognitionCapabilities.java new file mode 100644 index 000000000..ccdd300ea --- /dev/null +++ b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/config/provider/RecognitionCapabilities.java @@ -0,0 +1,37 @@ +package me.whereareiam.identica.provider.capability.recognition.config.provider; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import me.whereareiam.configura.annotation.merge.Merge; +import me.whereareiam.configura.feature.extension.api.annotation.ExtendableDocument; +import me.whereareiam.configura.merge.strategy.DeclaredObjectDefaults; +import me.whereareiam.identica.model.config.provider.Providers; +import me.whereareiam.identica.provider.capability.recognition.type.RecognitionSignal; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** + * Recognition capability provider-specific config extension. + */ +@Getter +@Setter +@ToString +public class RecognitionCapabilities extends Providers.ProviderEntry.Capabilities { + @Merge(DeclaredObjectDefaults.class) + @ExtendableDocument + private @Nullable Recognition recognition; + + @Getter + @Setter + @ToString + @ExtendableDocument + public static class Recognition { + private @Nullable Boolean enabled; + private @NotNull List signals = new ArrayList<>(); + private boolean allowOnUntrustedIps; + } +} diff --git a/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/config/provider/RecognitionProvidersProvider.java b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/config/provider/RecognitionProvidersProvider.java new file mode 100644 index 000000000..ceea5d534 --- /dev/null +++ b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/config/provider/RecognitionProvidersProvider.java @@ -0,0 +1,45 @@ +package me.whereareiam.identica.provider.capability.recognition.config.provider; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.google.inject.name.Named; +import me.whereareiam.configura.Config; +import me.whereareiam.configura.Configura; +import me.whereareiam.configura.document.DocumentTypeContext; +import me.whereareiam.configura.feature.extension.ExtensionFeature; +import me.whereareiam.configura.feature.extension.api.ConfigDocumentRule; +import me.whereareiam.identica.Registry; +import me.whereareiam.identica.Reloadable; +import me.whereareiam.identica.config.ConfigProvider; +import me.whereareiam.identica.model.config.provider.Providers; +import org.jetbrains.annotations.NotNull; + +import java.nio.file.Path; + +@Singleton +public class RecognitionProvidersProvider extends ConfigProvider { + @Inject + public RecognitionProvidersProvider( + @Named("providersPath") Path providersPath, + Registry reloadables + ) { + super(providersPath, "providers", Providers.class, reloadables); + } + + @Override + protected Configura configura() { + return Config.configured().withFeature(ExtensionFeature.rule( + ConfigDocumentRule.when( + Providers.ProviderEntry.Capabilities.class, + RecognitionCapabilities.class, + RecognitionProvidersProvider::isRecognitionCapabilitiesNode + ) + )); + } + + private static boolean isRecognitionCapabilitiesNode(@NotNull DocumentTypeContext context) { + return "capabilities".equals(context.getFieldName()) + && context.getCurrentNode() != null + && context.getCurrentNode().has("recognition"); + } +} diff --git a/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/config/provider/RecognitionSettingsProvider.java b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/config/provider/RecognitionSettingsProvider.java new file mode 100644 index 000000000..23210e438 --- /dev/null +++ b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/config/provider/RecognitionSettingsProvider.java @@ -0,0 +1,30 @@ +package me.whereareiam.identica.provider.capability.recognition.config.provider; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.google.inject.name.Named; +import me.whereareiam.configura.Config; +import me.whereareiam.configura.Configura; +import me.whereareiam.identica.Registry; +import me.whereareiam.identica.Reloadable; +import me.whereareiam.identica.config.ConfigProvider; +import me.whereareiam.identica.provider.capability.recognition.config.RecognitionSettings; +import me.whereareiam.identica.provider.capability.recognition.config.defaults.RecognitionSettingsDefaults; + +import java.nio.file.Path; + +@Singleton +public class RecognitionSettingsProvider extends ConfigProvider { + @Inject + public RecognitionSettingsProvider( + @Named("recognitionCapabilityPath") Path recognitionCapabilityPath, + Registry reloadables + ) { + super(recognitionCapabilityPath, "settings", RecognitionSettings.class, reloadables); + } + + @Override + protected Configura configura() { + return versioned(Config.configured().withDefaults(RecognitionSettingsDefaults.class), RecognitionSettings.class); + } +} diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/DefaultRecognitionEligibilityRegistry.java b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/eligibility/DefaultRecognitionEligibilityRegistry.java similarity index 79% rename from identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/DefaultRecognitionEligibilityRegistry.java rename to identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/eligibility/DefaultRecognitionEligibilityRegistry.java index 3bf7830c5..5d5d7a3ef 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/DefaultRecognitionEligibilityRegistry.java +++ b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/eligibility/DefaultRecognitionEligibilityRegistry.java @@ -1,8 +1,6 @@ -package me.whereareiam.identica.common.identity.session.recognition.eligibility; +package me.whereareiam.identica.provider.capability.recognition.eligibility; import com.google.inject.Singleton; -import me.whereareiam.identica.identity.session.recognition.eligibility.RecognitionEligibilityRegistry; -import me.whereareiam.identica.identity.session.recognition.eligibility.RecognitionEligibilityRule; import org.jetbrains.annotations.NotNull; import java.util.List; diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/DefaultRecognitionEligibilityService.java b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/eligibility/DefaultRecognitionEligibilityService.java similarity index 73% rename from identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/DefaultRecognitionEligibilityService.java rename to identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/eligibility/DefaultRecognitionEligibilityService.java index 5d701286b..eac516930 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/DefaultRecognitionEligibilityService.java +++ b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/eligibility/DefaultRecognitionEligibilityService.java @@ -1,14 +1,11 @@ -package me.whereareiam.identica.common.identity.session.recognition.eligibility; +package me.whereareiam.identica.provider.capability.recognition.eligibility; import com.google.inject.Inject; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.identity.session.recognition.eligibility.RecognitionEligibilityRegistry; -import me.whereareiam.identica.identity.session.recognition.eligibility.RecognitionEligibilityRule; -import me.whereareiam.identica.identity.session.recognition.eligibility.RecognitionEligibilityService; -import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityContext; -import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityDecision; -import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityRuleDecision; +import me.whereareiam.identica.provider.capability.recognition.model.eligibility.RecognitionEligibilityContext; +import me.whereareiam.identica.provider.capability.recognition.model.eligibility.RecognitionEligibilityDecision; +import me.whereareiam.identica.provider.capability.recognition.model.eligibility.RecognitionEligibilityRuleDecision; import org.jetbrains.annotations.NotNull; import java.util.Comparator; diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/matcher/IpMatcher.java b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/eligibility/matcher/IpMatcher.java similarity index 63% rename from identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/matcher/IpMatcher.java rename to identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/eligibility/matcher/IpMatcher.java index 867683c00..bfd47606f 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/matcher/IpMatcher.java +++ b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/eligibility/matcher/IpMatcher.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.common.identity.session.recognition.eligibility.matcher; +package me.whereareiam.identica.provider.capability.recognition.eligibility.matcher; import org.jetbrains.annotations.NotNull; diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/matcher/type/CidrMatcher.java b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/eligibility/matcher/type/CidrMatcher.java similarity index 81% rename from identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/matcher/type/CidrMatcher.java rename to identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/eligibility/matcher/type/CidrMatcher.java index a502cbe00..e3db085f6 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/matcher/type/CidrMatcher.java +++ b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/eligibility/matcher/type/CidrMatcher.java @@ -1,7 +1,7 @@ -package me.whereareiam.identica.common.identity.session.recognition.eligibility.matcher.type; +package me.whereareiam.identica.provider.capability.recognition.eligibility.matcher.type; import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.common.identity.session.recognition.eligibility.matcher.IpMatcher; +import me.whereareiam.identica.provider.capability.recognition.eligibility.matcher.IpMatcher; import org.jetbrains.annotations.NotNull; import java.net.InetAddress; diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/matcher/type/ExactIpMatcher.java b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/eligibility/matcher/type/ExactIpMatcher.java similarity index 64% rename from identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/matcher/type/ExactIpMatcher.java rename to identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/eligibility/matcher/type/ExactIpMatcher.java index 8e26c60c0..dcddc97b0 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/matcher/type/ExactIpMatcher.java +++ b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/eligibility/matcher/type/ExactIpMatcher.java @@ -1,7 +1,7 @@ -package me.whereareiam.identica.common.identity.session.recognition.eligibility.matcher.type; +package me.whereareiam.identica.provider.capability.recognition.eligibility.matcher.type; import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.common.identity.session.recognition.eligibility.matcher.IpMatcher; +import me.whereareiam.identica.provider.capability.recognition.eligibility.matcher.IpMatcher; import org.jetbrains.annotations.NotNull; import java.net.InetAddress; diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/rule/ExplicitSelectionRecognitionEligibilityRule.java b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/eligibility/rule/ExplicitSelectionRecognitionEligibilityRule.java similarity index 73% rename from identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/rule/ExplicitSelectionRecognitionEligibilityRule.java rename to identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/eligibility/rule/ExplicitSelectionRecognitionEligibilityRule.java index 5a30f1e9e..cda2c4a8f 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/rule/ExplicitSelectionRecognitionEligibilityRule.java +++ b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/eligibility/rule/ExplicitSelectionRecognitionEligibilityRule.java @@ -1,12 +1,12 @@ -package me.whereareiam.identica.common.identity.session.recognition.eligibility.rule; +package me.whereareiam.identica.provider.capability.recognition.eligibility.rule; import com.google.inject.Singleton; -import me.whereareiam.identica.identity.session.recognition.eligibility.RecognitionEligibilityRule; import me.whereareiam.identica.model.provider.ProviderContext; -import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityContext; -import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityRuleDecision; +import me.whereareiam.identica.provider.capability.recognition.eligibility.RecognitionEligibilityRule; +import me.whereareiam.identica.provider.capability.recognition.model.eligibility.RecognitionEligibilityContext; +import me.whereareiam.identica.provider.capability.recognition.model.eligibility.RecognitionEligibilityRuleDecision; +import me.whereareiam.identica.provider.capability.recognition.type.RecognitionAttemptKind; import me.whereareiam.identica.type.provider.ProviderOrigin; -import me.whereareiam.identica.type.session.recognition.RecognitionAttemptKind; import org.jetbrains.annotations.NotNull; @Singleton diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/rule/RecognitionEnabledEligibilityRule.java b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/eligibility/rule/RecognitionEnabledEligibilityRule.java similarity index 50% rename from identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/rule/RecognitionEnabledEligibilityRule.java rename to identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/eligibility/rule/RecognitionEnabledEligibilityRule.java index 0c0ab8ec0..2a6be07f5 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/rule/RecognitionEnabledEligibilityRule.java +++ b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/eligibility/rule/RecognitionEnabledEligibilityRule.java @@ -1,23 +1,25 @@ -package me.whereareiam.identica.common.identity.session.recognition.eligibility.rule; +package me.whereareiam.identica.provider.capability.recognition.eligibility.rule; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.identity.session.recognition.eligibility.RecognitionEligibilityRule; import me.whereareiam.identica.model.config.provider.Providers; -import me.whereareiam.identica.model.config.Settings; -import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityContext; -import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityRuleDecision; -import me.whereareiam.identica.type.session.recognition.RecognitionAttemptKind; +import me.whereareiam.identica.provider.capability.recognition.config.RecognitionSettings; +import me.whereareiam.identica.provider.capability.recognition.config.provider.RecognitionCapabilities; +import me.whereareiam.identica.provider.capability.recognition.config.provider.RecognitionProvidersProvider; +import me.whereareiam.identica.provider.capability.recognition.eligibility.RecognitionEligibilityRule; +import me.whereareiam.identica.provider.capability.recognition.model.eligibility.RecognitionEligibilityContext; +import me.whereareiam.identica.provider.capability.recognition.model.eligibility.RecognitionEligibilityRuleDecision; +import me.whereareiam.identica.provider.capability.recognition.type.RecognitionAttemptKind; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @Singleton @RequiredArgsConstructor(onConstructor_ = @Inject) public class RecognitionEnabledEligibilityRule implements RecognitionEligibilityRule { - private final Provider settingsProvider; - private final Provider providersProvider; + private final Provider settingsProvider; + private final RecognitionProvidersProvider providersProvider; @Override public @NotNull String id() { @@ -46,27 +48,27 @@ public boolean supports(@NotNull RecognitionEligibilityContext context) { } private boolean isRecognitionEnabled(@NotNull String providerId) { - Providers.ProviderEntry provider = findProvider(providerId); - Providers.ProviderEntry.Session session = provider != null - ? provider.getSession() - : null; - Providers.ProviderEntry.Session.Recognition recognition = session != null - ? session.getRecognition() - : null; - - Boolean override = recognition != null ? recognition.getEnabled() : null; + RecognitionCapabilities.Recognition provider = findProviderRecognition(providerId); + Boolean override = provider != null ? provider.getEnabled() : null; if (override != null) return override; - return settingsProvider.get().getSessions().getRecognition().isEnabled(); + return settingsProvider.get().isEnabled(); } - private @Nullable Providers.ProviderEntry findProvider(@Nullable String rawId) { + private @Nullable RecognitionCapabilities.Recognition findProviderRecognition(@Nullable String rawId) { if (rawId == null || rawId.isBlank()) return null; for (Providers.ProviderEntry provider : providersProvider.get().getProviders()) { if (provider == null || provider.getId().isBlank()) continue; - if (provider.getId().trim().equalsIgnoreCase(rawId.trim())) return provider; + if (!provider.getId().trim().equalsIgnoreCase(rawId.trim())) continue; + + Providers.ProviderEntry.Capabilities capabilities = provider.getCapabilities(); + if (!(capabilities instanceof RecognitionCapabilities recognitionCapabilities)) + return null; + + return recognitionCapabilities.getRecognition(); } + return null; } } diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/rule/UntrustedIpRecognitionEligibilityRule.java b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/eligibility/rule/UntrustedIpRecognitionEligibilityRule.java similarity index 72% rename from identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/rule/UntrustedIpRecognitionEligibilityRule.java rename to identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/eligibility/rule/UntrustedIpRecognitionEligibilityRule.java index 0b7b512dd..82ae18328 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/rule/UntrustedIpRecognitionEligibilityRule.java +++ b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/eligibility/rule/UntrustedIpRecognitionEligibilityRule.java @@ -1,17 +1,19 @@ -package me.whereareiam.identica.common.identity.session.recognition.eligibility.rule; +package me.whereareiam.identica.provider.capability.recognition.eligibility.rule; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.common.identity.session.recognition.eligibility.matcher.IpMatcher; -import me.whereareiam.identica.common.identity.session.recognition.eligibility.matcher.type.CidrMatcher; -import me.whereareiam.identica.common.identity.session.recognition.eligibility.matcher.type.ExactIpMatcher; -import me.whereareiam.identica.identity.session.recognition.eligibility.RecognitionEligibilityRule; import me.whereareiam.identica.model.config.provider.Providers; -import me.whereareiam.identica.model.config.Settings; -import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityContext; -import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityRuleDecision; +import me.whereareiam.identica.provider.capability.recognition.config.RecognitionSettings; +import me.whereareiam.identica.provider.capability.recognition.config.provider.RecognitionCapabilities; +import me.whereareiam.identica.provider.capability.recognition.config.provider.RecognitionProvidersProvider; +import me.whereareiam.identica.provider.capability.recognition.eligibility.RecognitionEligibilityRule; +import me.whereareiam.identica.provider.capability.recognition.eligibility.matcher.IpMatcher; +import me.whereareiam.identica.provider.capability.recognition.eligibility.matcher.type.CidrMatcher; +import me.whereareiam.identica.provider.capability.recognition.eligibility.matcher.type.ExactIpMatcher; +import me.whereareiam.identica.provider.capability.recognition.model.eligibility.RecognitionEligibilityContext; +import me.whereareiam.identica.provider.capability.recognition.model.eligibility.RecognitionEligibilityRuleDecision; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -22,8 +24,8 @@ @Singleton @RequiredArgsConstructor(onConstructor_ = @Inject) public class UntrustedIpRecognitionEligibilityRule implements RecognitionEligibilityRule { - private final Provider settingsProvider; - private final Provider providersProvider; + private final Provider settingsProvider; + private final RecognitionProvidersProvider providersProvider; @Override public @NotNull String id() { @@ -37,9 +39,7 @@ public int order() { @Override public @NotNull RecognitionEligibilityRuleDecision evaluate(@NotNull RecognitionEligibilityContext context) { - Settings.Sessions.Recognition.Eligibility.UntrustedIps untrustedIps = settingsProvider.get() - .getSessions() - .getRecognition() + RecognitionSettings.Eligibility.UntrustedIps untrustedIps = settingsProvider.get() .getEligibility() .getUntrustedIps(); @@ -63,14 +63,15 @@ public int order() { private boolean allowsRecognitionOverride(@Nullable String providerId) { if (providerId == null || providerId.isBlank()) return false; - for (Providers.ProviderEntry entry : providersProvider.get().getProviders()) { - if (entry == null) continue; - if (!entry.getId().equalsIgnoreCase(providerId)) continue; - Providers.ProviderEntry.Session session = entry.getSession(); - Providers.ProviderEntry.Session.Recognition recognition = session != null - ? session.getRecognition() - : null; + for (Providers.ProviderEntry provider : providersProvider.get().getProviders()) { + if (provider == null || provider.getId().isBlank()) continue; + if (!provider.getId().equalsIgnoreCase(providerId)) continue; + Providers.ProviderEntry.Capabilities capabilities = provider.getCapabilities(); + if (!(capabilities instanceof RecognitionCapabilities recognitionCapabilities)) + return false; + + RecognitionCapabilities.Recognition recognition = recognitionCapabilities.getRecognition(); return recognition != null && recognition.isAllowOnUntrustedIps(); } @@ -164,7 +165,7 @@ private boolean isHexDigit(int character) { private @NotNull IllegalStateException invalidEntry(int index, @Nullable String entry, @NotNull String reason) { return new IllegalStateException( - "settings.sessions.recognition.eligibility.untrustedIps.entries[" + index + "]=" + "providers.capabilities.recognition.settings.eligibility.untrustedIps.entries[" + index + "]=" + entry + " is invalid: " + reason diff --git a/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/pipeline/RecognitionAppliedLifecycle.java b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/pipeline/RecognitionAppliedLifecycle.java new file mode 100644 index 000000000..9d8ae10f8 --- /dev/null +++ b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/pipeline/RecognitionAppliedLifecycle.java @@ -0,0 +1,32 @@ +package me.whereareiam.identica.provider.capability.recognition.pipeline; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import lombok.RequiredArgsConstructor; +import me.whereareiam.identica.event.EventListener; +import me.whereareiam.identica.event.EventManager; +import me.whereareiam.identica.event.base.IdenticEvent; +import me.whereareiam.identica.event.session.SessionOpenedEvent; +import me.whereareiam.identica.provider.capability.recognition.event.RecognitionAppliedEvent; +import me.whereareiam.identica.provider.capability.recognition.store.RecognizedConnectionStore; +import me.whereareiam.identica.type.pipeline.PipelineType; +import org.jetbrains.annotations.NotNull; + +@Singleton +@RequiredArgsConstructor(onConstructor_ = @Inject) +public class RecognitionAppliedLifecycle implements EventListener { + private final EventManager eventManager; + private final RecognizedConnectionStore recognizedConnectionStore; + + @IdenticEvent + public void onSessionOpened(@NotNull SessionOpenedEvent event) { + if (event.getPipelineType() != PipelineType.AUTHENTICATION) return; + if (!recognizedConnectionStore.isRecognized(event.getConnectionUniqueId())) return; + + eventManager.call(new RecognitionAppliedEvent( + event.getConnectionUniqueId(), + event.getPipelineType(), + event.getSession() + )); + } +} diff --git a/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/pipeline/RecognitionPipelineExtension.java b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/pipeline/RecognitionPipelineExtension.java new file mode 100644 index 000000000..5268c2759 --- /dev/null +++ b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/pipeline/RecognitionPipelineExtension.java @@ -0,0 +1,43 @@ +package me.whereareiam.identica.provider.capability.recognition.pipeline; + +import com.google.inject.Inject; +import lombok.RequiredArgsConstructor; +import me.whereareiam.identica.model.pipeline.phase.PhasePlacement; +import me.whereareiam.identica.pipeline.extension.PipelineExtension; +import me.whereareiam.identica.pipeline.extension.PipelineExtensionBuilder; +import me.whereareiam.identica.provider.capability.recognition.pipeline.scenario.type.authentication.group.identity.phase.AuthenticationRecognitionSnapshotPhase; +import me.whereareiam.identica.provider.capability.recognition.pipeline.scenario.type.registration.group.identity.phase.RegistrationRecognitionSnapshotPhase; +import me.whereareiam.identica.type.pipeline.PipelineType; +import org.jetbrains.annotations.NotNull; + +@RequiredArgsConstructor(onConstructor_ = @Inject) +public class RecognitionPipelineExtension implements PipelineExtension { + private final AuthenticationRecognitionSnapshotPhase authenticationRecognitionSnapshotPhase; + private final RegistrationRecognitionSnapshotPhase registrationRecognitionSnapshotPhase; + + @Override + public @NotNull String id() { + return "recognition:phases"; + } + + @Override + public int order() { + return 10; + } + + @Override + public void apply(@NotNull PipelineExtensionBuilder builder) { + builder.registerScenarioPhase( + PipelineType.AUTHENTICATION, + "identity", + authenticationRecognitionSnapshotPhase, + PhasePlacement.after("refresh-provider-profile") + ); + builder.registerScenarioPhase( + PipelineType.REGISTRATION, + "identity", + registrationRecognitionSnapshotPhase, + PhasePlacement.after("link-provider") + ); + } +} diff --git a/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/pipeline/scenario/type/authentication/group/identity/phase/AuthenticationRecognitionSnapshotPhase.java b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/pipeline/scenario/type/authentication/group/identity/phase/AuthenticationRecognitionSnapshotPhase.java new file mode 100644 index 000000000..8ad79281c --- /dev/null +++ b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/pipeline/scenario/type/authentication/group/identity/phase/AuthenticationRecognitionSnapshotPhase.java @@ -0,0 +1,95 @@ +package me.whereareiam.identica.provider.capability.recognition.pipeline.scenario.type.authentication.group.identity.phase; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import lombok.RequiredArgsConstructor; +import me.whereareiam.identica.identity.actor.ConnectionIdentity; +import me.whereareiam.identica.model.auth.AuthContext; +import me.whereareiam.identica.model.identity.provider.AccountProviderProfile; +import me.whereareiam.identica.model.pipeline.PipelineResult; +import me.whereareiam.identica.model.pipeline.phase.PhaseResult; +import me.whereareiam.identica.model.provider.InternalProvider; +import me.whereareiam.identica.model.provider.ProviderContext; +import me.whereareiam.identica.pipeline.PipelinePhase; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.type.authentication.IdentityState; +import me.whereareiam.identica.provider.ProviderManager; +import me.whereareiam.identica.provider.capability.recognition.RecognitionCapability; +import me.whereareiam.identica.provider.capability.recognition.model.SessionRecognitionSnapshot; +import me.whereareiam.identica.provider.capability.recognition.store.SessionRecognitionStore; +import me.whereareiam.identica.type.pipeline.PipelineStatus; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +@Singleton +@RequiredArgsConstructor(onConstructor_ = @Inject) +public class AuthenticationRecognitionSnapshotPhase implements PipelinePhase { + private final SessionRecognitionStore sessionRecognitionStore; + private final ProviderManager providerManager; + + @Override + public @NotNull String id() { + return "recognition-authentication-snapshot"; + } + + @Override + public int order() { + return 260; + } + + @Override + public @NotNull Class stateType() { + return IdentityState.class; + } + + @Override + public @NotNull CompletionStage> execute( + @NotNull PipelineState pipelineState, + @NotNull IdentityState state + ) { + PipelineResult result = state.getResult(); + ProviderContext provider = state.getProvider(); + AccountProviderProfile profile = state.getProfile(); + AuthContext authContext = pipelineState.getScenario(pipelineState.getPipelineType()) instanceof AuthContext context + ? context + : null; + if (result == null + || result.getStatus() != PipelineStatus.COMPLETE + || provider == null + || profile == null + || authContext == null + || !supportsRecognition(provider.getProviderId())) + return CompletableFuture.completedFuture(PhaseResult.pass(state)); + + ConnectionIdentity.Origin origin = authContext.getIdentity().getOrigin(); + long now = System.currentTimeMillis(); + sessionRecognitionStore.save(SessionRecognitionSnapshot.builder() + .providerId(profile.getProviderId()) + .providerSubject(profile.getProviderSubject()) + .providerUsername(profile.getProviderUsername()) + .lastIp(authContext.getIp()) + .lastVirtualHost(origin != null ? origin.getHost() : null) + .lastVirtualPort(origin != null ? origin.getPort() : null) + .capturedAt(now) + .build()); + return CompletableFuture.completedFuture(PhaseResult.pass(state)); + } + + private boolean supportsRecognition(String providerId) { + if (providerId == null || providerId.isBlank()) + return false; + + for (InternalProvider provider : providerManager.getProviders()) { + if (provider == null || provider.getDescriptor() == null) + continue; + if (!providerId.equalsIgnoreCase(provider.getDescriptor().getId())) + continue; + + return provider.getDescriptor().hasCapability(RecognitionCapability.CAPABILITY); + } + + return false; + } +} diff --git a/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/pipeline/scenario/type/registration/group/identity/phase/RegistrationRecognitionSnapshotPhase.java b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/pipeline/scenario/type/registration/group/identity/phase/RegistrationRecognitionSnapshotPhase.java new file mode 100644 index 000000000..886af14d7 --- /dev/null +++ b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/pipeline/scenario/type/registration/group/identity/phase/RegistrationRecognitionSnapshotPhase.java @@ -0,0 +1,88 @@ +package me.whereareiam.identica.provider.capability.recognition.pipeline.scenario.type.registration.group.identity.phase; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import lombok.RequiredArgsConstructor; +import me.whereareiam.identica.identity.actor.ConnectionIdentity; +import me.whereareiam.identica.model.identity.provider.AccountProviderProfile; +import me.whereareiam.identica.model.pipeline.PipelineResult; +import me.whereareiam.identica.model.pipeline.phase.PhaseResult; +import me.whereareiam.identica.model.provider.InternalProvider; +import me.whereareiam.identica.model.registration.RegistrationContext; +import me.whereareiam.identica.pipeline.PipelinePhase; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.type.registration.IdentityState; +import me.whereareiam.identica.provider.ProviderManager; +import me.whereareiam.identica.provider.capability.recognition.RecognitionCapability; +import me.whereareiam.identica.provider.capability.recognition.model.SessionRecognitionSnapshot; +import me.whereareiam.identica.provider.capability.recognition.store.SessionRecognitionStore; +import me.whereareiam.identica.type.pipeline.PipelineStatus; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +@Singleton +@RequiredArgsConstructor(onConstructor_ = @Inject) +public class RegistrationRecognitionSnapshotPhase implements PipelinePhase { + private final SessionRecognitionStore sessionRecognitionStore; + private final ProviderManager providerManager; + + @Override + public @NotNull String id() { + return "recognition-registration-snapshot"; + } + + @Override + public int order() { + return 410; + } + + @Override + public @NotNull Class stateType() { + return IdentityState.class; + } + + @Override + public @NotNull CompletionStage> execute( + @NotNull PipelineState pipelineState, + @NotNull IdentityState state + ) { + PipelineResult result = state.getResult(); + AccountProviderProfile profile = state.getProfile(); + RegistrationContext context = state.getContext(); + if (result == null + || result.getStatus() != PipelineStatus.COMPLETE + || profile == null + || context == null + || !supportsRecognition(profile.getProviderId())) + return CompletableFuture.completedFuture(PhaseResult.pass(state)); + + ConnectionIdentity.Origin origin = context.getIdentity().getOrigin(); + long now = System.currentTimeMillis(); + sessionRecognitionStore.save(SessionRecognitionSnapshot.builder() + .providerId(profile.getProviderId()) + .providerSubject(profile.getProviderSubject()) + .providerUsername(profile.getProviderUsername()) + .lastIp(context.getIp()) + .lastVirtualHost(origin != null ? origin.getHost() : null) + .lastVirtualPort(origin != null ? origin.getPort() : null) + .capturedAt(now) + .build()); + return CompletableFuture.completedFuture(PhaseResult.pass(state)); + } + + private boolean supportsRecognition(String providerId) { + if (providerId == null || providerId.isBlank()) + return false; + + for (InternalProvider provider : providerManager.getProviders()) { + if (provider == null || provider.getDescriptor() == null) continue; + if (!providerId.equalsIgnoreCase(provider.getDescriptor().getId()))continue; + + return provider.getDescriptor().hasCapability(RecognitionCapability.CAPABILITY); + } + + return false; + } +} diff --git a/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/store/DefaultRecognizedConnectionStore.java b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/store/DefaultRecognizedConnectionStore.java new file mode 100644 index 000000000..620a6adab --- /dev/null +++ b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/store/DefaultRecognizedConnectionStore.java @@ -0,0 +1,65 @@ +package me.whereareiam.identica.provider.capability.recognition.store; + +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import me.whereareiam.identica.model.config.Replication; +import me.whereareiam.identica.model.replication.ReplicationType; +import me.whereareiam.identica.provider.capability.recognition.config.RecognitionSettings; +import me.whereareiam.identica.replication.ReplicationSystem; +import me.whereareiam.identica.replication.cache.base.Cache; +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; +import java.util.UUID; + +@Singleton +public class DefaultRecognizedConnectionStore implements RecognizedConnectionStore { + private final Cache cache; + private final Provider settingsProvider; + + @Inject + public DefaultRecognizedConnectionStore( + @NotNull ReplicationSystem replicationSystem, + @NotNull Provider settingsProvider, + @NotNull Provider replicationProvider + ) { + this.settingsProvider = settingsProvider; + RecognitionSettings settings = settingsProvider.get(); + long ttlMs = settings.windowMillis(); + + String namespace = resolveNamespace(settings); + this.cache = replicationProvider.get().isEnabled() + ? replicationSystem.cache(namespace) + .defaultTtl(ttlMs) + .replicated(ReplicationType.identity(Boolean.class)) + : replicationSystem.cache(namespace) + .defaultTtl(ttlMs) + .local(); + } + + @Override + public void markRecognized(@NotNull UUID connectionUniqueId) { + cache.put(connectionUniqueId.toString(), Boolean.TRUE, settingsProvider.get().windowMillis()).join(); + } + + @Override + public boolean isRecognized(@NotNull UUID connectionUniqueId) { + Optional stored = cache.get(connectionUniqueId.toString()).join(); + return stored.orElse(false); + } + + @Override + public boolean consumeRecognized(@NotNull UUID connectionUniqueId) { + Optional stored = cache.consume(connectionUniqueId.toString()).join(); + return stored.orElse(false); + } + + private static @NotNull String resolveNamespace(@NotNull RecognitionSettings settings) { + String namespace = settings.getReplication().getRecognizedConnectionNamespace(); + if (namespace.isBlank()) + throw new IllegalStateException("providers.capabilities.recognition.settings.replication.recognizedConnectionNamespace is missing"); + + return namespace; + } +} diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/DefaultSessionRecognitionStore.java b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/store/DefaultSessionRecognitionStore.java similarity index 70% rename from identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/DefaultSessionRecognitionStore.java rename to identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/store/DefaultSessionRecognitionStore.java index 62a180370..01e0898a0 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/DefaultSessionRecognitionStore.java +++ b/identica-capability/capability-recognition/recognition-common/src/main/java/me/whereareiam/identica/provider/capability/recognition/store/DefaultSessionRecognitionStore.java @@ -1,13 +1,12 @@ -package me.whereareiam.identica.common.identity.session.recognition; +package me.whereareiam.identica.provider.capability.recognition.store; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; -import me.whereareiam.identica.identity.session.recognition.SessionRecognitionStore; import me.whereareiam.identica.model.config.Replication; -import me.whereareiam.identica.model.config.Settings; import me.whereareiam.identica.model.replication.ReplicationType; -import me.whereareiam.identica.model.session.SessionRecognitionSnapshot; +import me.whereareiam.identica.provider.capability.recognition.config.RecognitionSettings; +import me.whereareiam.identica.provider.capability.recognition.model.SessionRecognitionSnapshot; import me.whereareiam.identica.replication.ReplicationSystem; import me.whereareiam.identica.replication.cache.ReplicatedCache; import org.jetbrains.annotations.NotNull; @@ -22,15 +21,14 @@ public class DefaultSessionRecognitionStore implements SessionRecognitionStore { @Inject public DefaultSessionRecognitionStore( - @NotNull Provider settingsProvider, - @NotNull Provider replicationProvider, + @NotNull Provider settingsProvider, + @NotNull Provider ignoredReplicationProvider, @NotNull ReplicationSystem replicationSystem ) { - Replication.Sessions.Recognition recognition = replicationProvider.get().getCache().getSessions().getRecognition(); - long ttlMs = settingsProvider.get().getSessions().getRecognition().validityMillis(); + long ttlMs = settingsProvider.get().validityMillis(); ReplicationType type = ReplicationType.identity(SessionRecognitionSnapshot.class); - this.cache = replicationSystem.cache(resolveNamespace(recognition)) + this.cache = replicationSystem.cache(resolveNamespace(settingsProvider.get())) .defaultTtl(ttlMs) .replicated(type); } @@ -85,9 +83,10 @@ public void clear( return value.trim().toLowerCase(Locale.ROOT); } - private static @NotNull String resolveNamespace(@NotNull Replication.Sessions.Recognition recognition) { - String namespace = recognition.getSnapshot(); - if (namespace.isBlank()) throw new IllegalStateException("replication.cache.sessions.recognition.snapshot is missing"); + private static @NotNull String resolveNamespace(@NotNull RecognitionSettings settings) { + String namespace = settings.getReplication().getSnapshotNamespace(); + if (namespace.isBlank()) + throw new IllegalStateException("providers.capabilities.recognition.settings.replication.snapshotNamespace is missing"); return namespace; } diff --git a/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/recognition/DefaultSessionRecognitionServiceTest.java b/identica-capability/capability-recognition/recognition-common/src/test/java/me/whereareiam/identica/provider/capability/recognition/DefaultSessionRecognitionServiceTest.java similarity index 52% rename from identica-common/src/test/java/me/whereareiam/identica/common/identity/session/recognition/DefaultSessionRecognitionServiceTest.java rename to identica-capability/capability-recognition/recognition-common/src/test/java/me/whereareiam/identica/provider/capability/recognition/DefaultSessionRecognitionServiceTest.java index b1f421c99..e43ee8449 100644 --- a/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/recognition/DefaultSessionRecognitionServiceTest.java +++ b/identica-capability/capability-recognition/recognition-common/src/test/java/me/whereareiam/identica/provider/capability/recognition/DefaultSessionRecognitionServiceTest.java @@ -1,15 +1,17 @@ -package me.whereareiam.identica.common.identity.session.recognition; +package me.whereareiam.identica.provider.capability.recognition; -import me.whereareiam.identica.common.identity.session.recognition.eligibility.DefaultRecognitionEligibilityRegistry; -import me.whereareiam.identica.common.identity.session.recognition.eligibility.DefaultRecognitionEligibilityService; -import me.whereareiam.identica.common.identity.session.recognition.eligibility.rule.RecognitionEnabledEligibilityRule; -import me.whereareiam.identica.common.identity.session.recognition.eligibility.rule.UntrustedIpRecognitionEligibilityRule; import me.whereareiam.identica.identity.actor.ConnectionIdentity; -import me.whereareiam.identica.identity.session.recognition.SessionRecognitionStore; import me.whereareiam.identica.model.config.provider.Providers; -import me.whereareiam.identica.model.config.Settings; -import me.whereareiam.identica.model.session.SessionRecognitionSnapshot; -import me.whereareiam.identica.type.session.recognition.RecognitionSignal; +import me.whereareiam.identica.provider.capability.recognition.config.RecognitionSettings; +import me.whereareiam.identica.provider.capability.recognition.config.provider.RecognitionCapabilities; +import me.whereareiam.identica.provider.capability.recognition.config.provider.RecognitionProvidersProvider; +import me.whereareiam.identica.provider.capability.recognition.eligibility.DefaultRecognitionEligibilityRegistry; +import me.whereareiam.identica.provider.capability.recognition.eligibility.DefaultRecognitionEligibilityService; +import me.whereareiam.identica.provider.capability.recognition.eligibility.rule.RecognitionEnabledEligibilityRule; +import me.whereareiam.identica.provider.capability.recognition.eligibility.rule.UntrustedIpRecognitionEligibilityRule; +import me.whereareiam.identica.provider.capability.recognition.model.SessionRecognitionSnapshot; +import me.whereareiam.identica.provider.capability.recognition.store.SessionRecognitionStore; +import me.whereareiam.identica.provider.capability.recognition.type.RecognitionSignal; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -27,11 +29,11 @@ class DefaultSessionRecognitionServiceTest { @DisplayName("Blocks provider recognition on an untrusted IP even when the stored snapshot matches") @Test void blocksRecognitionOnUntrustedIp() { - Settings settings = settings(List.of(RecognitionSignal.USERNAME, RecognitionSignal.IP), List.of("127.0.0.1")); - Providers providers = providers(false); + RecognitionSettings settings = settings(List.of(RecognitionSignal.USERNAME, RecognitionSignal.IP), List.of("127.0.0.1")); + RecognitionProvidersProvider providersProvider = providersProvider(false); SessionRecognitionStore store = store(snapshot("127.0.0.1", null, null)); - DefaultSessionRecognitionService service = service(settings, providers, store); + DefaultSessionRecognitionService service = service(settings, providersProvider, store); assertFalse(service.matches("credential", "subject-1", "whereareiam", "127.0.0.1", null)); } @@ -39,11 +41,11 @@ void blocksRecognitionOnUntrustedIp() { @DisplayName("Allows provider recognition on an untrusted IP when provider recognition allows it") @Test void allowsRecognitionOnUntrustedIpWhenProviderRecognitionAllowsIt() { - Settings settings = settings(List.of(RecognitionSignal.USERNAME, RecognitionSignal.IP), List.of("127.0.0.1")); - Providers providers = providers(true); + RecognitionSettings settings = settings(List.of(RecognitionSignal.USERNAME, RecognitionSignal.IP), List.of("127.0.0.1")); + RecognitionProvidersProvider providersProvider = providersProvider(true); SessionRecognitionStore store = store(snapshot("127.0.0.1", null, null)); - DefaultSessionRecognitionService service = service(settings, providers, store); + DefaultSessionRecognitionService service = service(settings, providersProvider, store); assertTrue(service.matches("credential", "subject-1", "whereareiam", "127.0.0.1", null)); } @@ -51,14 +53,14 @@ void allowsRecognitionOnUntrustedIpWhenProviderRecognitionAllowsIt() { @DisplayName("Keeps trusted IP recognition working for matching snapshots") @Test void keepsTrustedIpRecognitionWorking() { - Settings settings = settings( + RecognitionSettings settings = settings( List.of(RecognitionSignal.USERNAME, RecognitionSignal.IP, RecognitionSignal.VIRTUAL_HOST), List.of("127.0.0.1") ); - Providers providers = providers(false); + RecognitionProvidersProvider providersProvider = providersProvider(false); SessionRecognitionStore store = store(snapshot("203.0.113.10", "localhost", 25565)); - DefaultSessionRecognitionService service = service(settings, providers, store); + DefaultSessionRecognitionService service = service(settings, providersProvider, store); assertTrue(service.matches( "credential", @@ -70,17 +72,17 @@ void keepsTrustedIpRecognitionWorking() { } private DefaultSessionRecognitionService service( - Settings settings, - Providers providers, + RecognitionSettings settings, + RecognitionProvidersProvider providersProvider, SessionRecognitionStore store ) { DefaultRecognitionEligibilityRegistry registry = new DefaultRecognitionEligibilityRegistry(); - registry.register(new RecognitionEnabledEligibilityRule(() -> settings, () -> providers)); - registry.register(new UntrustedIpRecognitionEligibilityRule(() -> settings, () -> providers)); + registry.register(new RecognitionEnabledEligibilityRule(() -> settings, providersProvider)); + registry.register(new UntrustedIpRecognitionEligibilityRule(() -> settings, providersProvider)); return new DefaultSessionRecognitionService( () -> settings, - () -> providers, + providersProvider, store, new DefaultRecognitionEligibilityService(registry) ); @@ -92,44 +94,39 @@ private SessionRecognitionStore store(SessionRecognitionSnapshot snapshot) { return store; } - private Settings settings( + private RecognitionSettings settings( List signals, List untrustedIps ) { - Settings.Sessions.Recognition.Eligibility.UntrustedIps untrusted = - new Settings.Sessions.Recognition.Eligibility.UntrustedIps(); + RecognitionSettings.Eligibility.UntrustedIps untrusted = new RecognitionSettings.Eligibility.UntrustedIps(); untrusted.setEnabled(true); untrusted.setEntries(untrustedIps); - Settings.Sessions.Recognition.Eligibility eligibility = new Settings.Sessions.Recognition.Eligibility(); + RecognitionSettings.Eligibility eligibility = new RecognitionSettings.Eligibility(); eligibility.setUntrustedIps(untrusted); - Settings.Sessions.Recognition recognition = new Settings.Sessions.Recognition(); - recognition.setEnabled(true); - recognition.setValidity(Duration.ofHours(12)); - recognition.setDefaultSignals(signals); - recognition.setEligibility(eligibility); - - Settings.Sessions sessions = new Settings.Sessions(); - sessions.setRecognition(recognition); - - Settings settings = new Settings(); - settings.setSessions(sessions); + RecognitionSettings settings = new RecognitionSettings(); + settings.setEnabled(true); + settings.setValidity(Duration.ofHours(12)); + settings.setDefaultSignals(signals); + settings.setEligibility(eligibility); return settings; } - private Providers providers(boolean allowRecognitionOnUntrustedIp) { - Providers.ProviderEntry provider = new Providers.ProviderEntry(); - provider.setId("credential"); - Providers.ProviderEntry.Session.Recognition recognition = new Providers.ProviderEntry.Session.Recognition(); - recognition.setAllowOnUntrustedIps(allowRecognitionOnUntrustedIp); - Providers.ProviderEntry.Session session = new Providers.ProviderEntry.Session(); - session.setRecognition(recognition); - provider.setSession(session); - + private RecognitionProvidersProvider providersProvider(boolean allowOnUntrustedIps) { + RecognitionProvidersProvider provider = mock(RecognitionProvidersProvider.class); Providers providers = new Providers(); - providers.setProviders(List.of(provider)); - return providers; + Providers.ProviderEntry credential = new Providers.ProviderEntry(); + credential.setId("credential"); + RecognitionCapabilities capabilities = new RecognitionCapabilities(); + RecognitionCapabilities.Recognition recognition = new RecognitionCapabilities.Recognition(); + recognition.setEnabled(true); + recognition.setAllowOnUntrustedIps(allowOnUntrustedIps); + capabilities.setRecognition(recognition); + credential.setDeclaredCapabilityIds(capabilities); + providers.setProviders(List.of(credential)); + when(provider.get()).thenReturn(providers); + return provider; } private SessionRecognitionSnapshot snapshot( diff --git a/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/DefaultRecognitionEligibilityRegistryTest.java b/identica-capability/capability-recognition/recognition-common/src/test/java/me/whereareiam/identica/provider/capability/recognition/eligibility/DefaultRecognitionEligibilityRegistryTest.java similarity index 80% rename from identica-common/src/test/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/DefaultRecognitionEligibilityRegistryTest.java rename to identica-capability/capability-recognition/recognition-common/src/test/java/me/whereareiam/identica/provider/capability/recognition/eligibility/DefaultRecognitionEligibilityRegistryTest.java index aaccb3f8f..fd2513163 100644 --- a/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/DefaultRecognitionEligibilityRegistryTest.java +++ b/identica-capability/capability-recognition/recognition-common/src/test/java/me/whereareiam/identica/provider/capability/recognition/eligibility/DefaultRecognitionEligibilityRegistryTest.java @@ -1,8 +1,7 @@ -package me.whereareiam.identica.common.identity.session.recognition.eligibility; +package me.whereareiam.identica.provider.capability.recognition.eligibility; -import me.whereareiam.identica.identity.session.recognition.eligibility.RecognitionEligibilityRule; -import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityContext; -import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityRuleDecision; +import me.whereareiam.identica.provider.capability.recognition.model.eligibility.RecognitionEligibilityContext; +import me.whereareiam.identica.provider.capability.recognition.model.eligibility.RecognitionEligibilityRuleDecision; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/DefaultRecognitionEligibilityServiceTest.java b/identica-capability/capability-recognition/recognition-common/src/test/java/me/whereareiam/identica/provider/capability/recognition/eligibility/DefaultRecognitionEligibilityServiceTest.java similarity index 84% rename from identica-common/src/test/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/DefaultRecognitionEligibilityServiceTest.java rename to identica-capability/capability-recognition/recognition-common/src/test/java/me/whereareiam/identica/provider/capability/recognition/eligibility/DefaultRecognitionEligibilityServiceTest.java index 27f68a14b..f72f0c16c 100644 --- a/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/DefaultRecognitionEligibilityServiceTest.java +++ b/identica-capability/capability-recognition/recognition-common/src/test/java/me/whereareiam/identica/provider/capability/recognition/eligibility/DefaultRecognitionEligibilityServiceTest.java @@ -1,11 +1,10 @@ -package me.whereareiam.identica.common.identity.session.recognition.eligibility; - -import me.whereareiam.identica.identity.session.recognition.eligibility.RecognitionEligibilityRule; -import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityContext; -import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityDecision; -import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityRuleDecision; -import me.whereareiam.identica.type.session.recognition.RecognitionAttemptKind; -import me.whereareiam.identica.type.session.recognition.RecognitionTrigger; +package me.whereareiam.identica.provider.capability.recognition.eligibility; + +import me.whereareiam.identica.provider.capability.recognition.model.eligibility.RecognitionEligibilityContext; +import me.whereareiam.identica.provider.capability.recognition.model.eligibility.RecognitionEligibilityDecision; +import me.whereareiam.identica.provider.capability.recognition.model.eligibility.RecognitionEligibilityRuleDecision; +import me.whereareiam.identica.provider.capability.recognition.type.RecognitionAttemptKind; +import me.whereareiam.identica.provider.capability.recognition.type.RecognitionTrigger; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/rule/ExplicitSelectionRecognitionEligibilityRuleTest.java b/identica-capability/capability-recognition/recognition-common/src/test/java/me/whereareiam/identica/provider/capability/recognition/eligibility/rule/ExplicitSelectionRecognitionEligibilityRuleTest.java similarity index 77% rename from identica-common/src/test/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/rule/ExplicitSelectionRecognitionEligibilityRuleTest.java rename to identica-capability/capability-recognition/recognition-common/src/test/java/me/whereareiam/identica/provider/capability/recognition/eligibility/rule/ExplicitSelectionRecognitionEligibilityRuleTest.java index e52b4d381..54fc8da71 100644 --- a/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/rule/ExplicitSelectionRecognitionEligibilityRuleTest.java +++ b/identica-capability/capability-recognition/recognition-common/src/test/java/me/whereareiam/identica/provider/capability/recognition/eligibility/rule/ExplicitSelectionRecognitionEligibilityRuleTest.java @@ -1,11 +1,11 @@ -package me.whereareiam.identica.common.identity.session.recognition.eligibility.rule; +package me.whereareiam.identica.provider.capability.recognition.eligibility.rule; import me.whereareiam.identica.model.provider.ProviderContext; -import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityContext; -import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityRuleDecision; +import me.whereareiam.identica.provider.capability.recognition.model.eligibility.RecognitionEligibilityContext; +import me.whereareiam.identica.provider.capability.recognition.model.eligibility.RecognitionEligibilityRuleDecision; +import me.whereareiam.identica.provider.capability.recognition.type.RecognitionAttemptKind; +import me.whereareiam.identica.provider.capability.recognition.type.RecognitionTrigger; import me.whereareiam.identica.type.provider.ProviderOrigin; -import me.whereareiam.identica.type.session.recognition.RecognitionAttemptKind; -import me.whereareiam.identica.type.session.recognition.RecognitionTrigger; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/identica-capability/capability-recognition/recognition-common/src/test/java/me/whereareiam/identica/provider/capability/recognition/eligibility/rule/RecognitionEnabledEligibilityRuleTest.java b/identica-capability/capability-recognition/recognition-common/src/test/java/me/whereareiam/identica/provider/capability/recognition/eligibility/rule/RecognitionEnabledEligibilityRuleTest.java new file mode 100644 index 000000000..11105f808 --- /dev/null +++ b/identica-capability/capability-recognition/recognition-common/src/test/java/me/whereareiam/identica/provider/capability/recognition/eligibility/rule/RecognitionEnabledEligibilityRuleTest.java @@ -0,0 +1,77 @@ +package me.whereareiam.identica.provider.capability.recognition.eligibility.rule; + +import me.whereareiam.identica.model.config.provider.Providers; +import me.whereareiam.identica.provider.capability.recognition.config.RecognitionSettings; +import me.whereareiam.identica.provider.capability.recognition.config.provider.RecognitionCapabilities; +import me.whereareiam.identica.provider.capability.recognition.config.provider.RecognitionProvidersProvider; +import me.whereareiam.identica.provider.capability.recognition.model.eligibility.RecognitionEligibilityContext; +import me.whereareiam.identica.provider.capability.recognition.model.eligibility.RecognitionEligibilityRuleDecision; +import me.whereareiam.identica.provider.capability.recognition.type.RecognitionAttemptKind; +import me.whereareiam.identica.provider.capability.recognition.type.RecognitionTrigger; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.Duration; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@DisplayName("Recognition Enabled Eligibility Rule") +class RecognitionEnabledEligibilityRuleTest { + @DisplayName("Blocks session recognition when recognition is disabled") + @Test + void blocksSessionRecognitionWhenRecognitionIsDisabled() { + RecognitionSettings settings = settings(); + RecognitionProvidersProvider providersProvider = providersProvider(null); + + RecognitionEligibilityRuleDecision decision = new RecognitionEnabledEligibilityRule(() -> settings, providersProvider) + .evaluate(context()); + + assertEquals(RecognitionEligibilityRuleDecision.Status.BLOCK, decision.getStatus()); + } + + @DisplayName("Provider recognition settings restore recognition when enabled") + @Test + void providerRecognitionSettingsRestoreRecognitionWhenEnabled() { + RecognitionSettings settings = settings(); + RecognitionProvidersProvider providersProvider = providersProvider(true); + + RecognitionEligibilityRuleDecision decision = new RecognitionEnabledEligibilityRule(() -> settings, providersProvider) + .evaluate(context()); + + assertEquals(RecognitionEligibilityRuleDecision.Status.ABSTAIN, decision.getStatus()); + } + + private RecognitionEligibilityContext context() { + return RecognitionEligibilityContext.builder() + .providerId("premium") + .attemptKind(RecognitionAttemptKind.SESSION_RECOGNITION) + .trigger(RecognitionTrigger.AUTOMATIC) + .build(); + } + + private RecognitionSettings settings() { + RecognitionSettings settings = new RecognitionSettings(); + settings.setEnabled(false); + settings.setValidity(Duration.ofHours(12)); + return settings; + } + + private RecognitionProvidersProvider providersProvider(Boolean enabledOverride) { + RecognitionProvidersProvider provider = mock(RecognitionProvidersProvider.class); + Providers providers = new Providers(); + Providers.ProviderEntry premium = new Providers.ProviderEntry(); + premium.setId("premium"); + if (enabledOverride != null) { + RecognitionCapabilities capabilities = new RecognitionCapabilities(); + RecognitionCapabilities.Recognition recognition = new RecognitionCapabilities.Recognition(); + recognition.setEnabled(enabledOverride); + capabilities.setRecognition(recognition); + premium.setDeclaredCapabilityIds(capabilities); + } + providers.setProviders(java.util.List.of(premium)); + when(provider.get()).thenReturn(providers); + return provider; + } +} diff --git a/identica-capability/capability-recognition/recognition-common/src/test/java/me/whereareiam/identica/provider/capability/recognition/eligibility/rule/UntrustedIpRecognitionEligibilityRuleTest.java b/identica-capability/capability-recognition/recognition-common/src/test/java/me/whereareiam/identica/provider/capability/recognition/eligibility/rule/UntrustedIpRecognitionEligibilityRuleTest.java new file mode 100644 index 000000000..6c8956231 --- /dev/null +++ b/identica-capability/capability-recognition/recognition-common/src/test/java/me/whereareiam/identica/provider/capability/recognition/eligibility/rule/UntrustedIpRecognitionEligibilityRuleTest.java @@ -0,0 +1,95 @@ +package me.whereareiam.identica.provider.capability.recognition.eligibility.rule; + +import me.whereareiam.identica.model.config.provider.Providers; +import me.whereareiam.identica.provider.capability.recognition.config.RecognitionSettings; +import me.whereareiam.identica.provider.capability.recognition.config.provider.RecognitionCapabilities; +import me.whereareiam.identica.provider.capability.recognition.config.provider.RecognitionProvidersProvider; +import me.whereareiam.identica.provider.capability.recognition.model.eligibility.RecognitionEligibilityContext; +import me.whereareiam.identica.provider.capability.recognition.model.eligibility.RecognitionEligibilityRuleDecision; +import me.whereareiam.identica.provider.capability.recognition.type.RecognitionAttemptKind; +import me.whereareiam.identica.provider.capability.recognition.type.RecognitionTrigger; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@DisplayName("Untrusted IP Recognition Eligibility Rule") +class UntrustedIpRecognitionEligibilityRuleTest { + @DisplayName("Matches exact IPv4 entries") + @Test + void matchesExactIpv4Entries() { + RecognitionEligibilityRuleDecision decision = rule(settings(List.of("203.0.113.10")), providersProvider(false)) + .evaluate(context("203.0.113.10")); + + assertEquals(RecognitionEligibilityRuleDecision.Status.BLOCK, decision.getStatus()); + } + + @DisplayName("Provider recognition settings allow recognition on untrusted IPs") + @Test + void providerRecognitionSettingsAllowRecognitionOnUntrustedIps() { + RecognitionSettings settings = settings(List.of("203.0.113.10")); + RecognitionEligibilityRuleDecision decision = rule(settings, providersProvider(true)) + .evaluate(context("203.0.113.10")); + + assertEquals(RecognitionEligibilityRuleDecision.Status.ALLOW, decision.getStatus()); + } + + @DisplayName("Invalid entries fail with the eligibility config path") + @Test + void invalidEntriesFailWithEligibilityConfigPath() { + IllegalStateException exception = assertThrows( + IllegalStateException.class, + () -> rule(settings(List.of("not-an-ip")), providersProvider(false)).evaluate(context("203.0.113.10")) + ); + + assertTrue(exception.getMessage().contains("providers.capabilities.recognition.settings.eligibility.untrustedIps.entries[0]")); + } + + private UntrustedIpRecognitionEligibilityRule rule( + RecognitionSettings settings, + RecognitionProvidersProvider providersProvider + ) { + return new UntrustedIpRecognitionEligibilityRule(() -> settings, providersProvider); + } + + private RecognitionEligibilityContext context(String ip) { + return RecognitionEligibilityContext.builder() + .providerId("premium") + .clientIp(ip) + .attemptKind(RecognitionAttemptKind.SESSION_RECOGNITION) + .trigger(RecognitionTrigger.AUTOMATIC) + .build(); + } + + private RecognitionSettings settings(List entries) { + RecognitionSettings.Eligibility.UntrustedIps untrustedIps = new RecognitionSettings.Eligibility.UntrustedIps(); + untrustedIps.setEnabled(true); + untrustedIps.setEntries(entries); + + RecognitionSettings.Eligibility eligibility = new RecognitionSettings.Eligibility(); + eligibility.setUntrustedIps(untrustedIps); + + RecognitionSettings settings = new RecognitionSettings(); + settings.setEligibility(eligibility); + return settings; + } + + private RecognitionProvidersProvider providersProvider(boolean allowOnUntrustedIp) { + RecognitionProvidersProvider provider = mock(RecognitionProvidersProvider.class); + Providers providers = new Providers(); + Providers.ProviderEntry premium = new Providers.ProviderEntry(); + premium.setId("premium"); + RecognitionCapabilities capabilities = new RecognitionCapabilities(); + RecognitionCapabilities.Recognition recognition = new RecognitionCapabilities.Recognition(); + recognition.setAllowOnUntrustedIps(allowOnUntrustedIp); + capabilities.setRecognition(recognition); + premium.setDeclaredCapabilityIds(capabilities); + providers.setProviders(List.of(premium)); + when(provider.get()).thenReturn(providers); + return provider; + } +} diff --git a/identica-capability/capability-recognition/recognition-common/src/test/java/me/whereareiam/identica/provider/capability/recognition/store/DefaultRecognizedConnectionStoreTest.java b/identica-capability/capability-recognition/recognition-common/src/test/java/me/whereareiam/identica/provider/capability/recognition/store/DefaultRecognizedConnectionStoreTest.java new file mode 100644 index 000000000..9365d758c --- /dev/null +++ b/identica-capability/capability-recognition/recognition-common/src/test/java/me/whereareiam/identica/provider/capability/recognition/store/DefaultRecognizedConnectionStoreTest.java @@ -0,0 +1,196 @@ +package me.whereareiam.identica.provider.capability.recognition.store; + +import me.whereareiam.identica.model.config.Replication; +import me.whereareiam.identica.model.replication.ReplicationType; +import me.whereareiam.identica.provider.capability.recognition.config.RecognitionSettings; +import me.whereareiam.identica.replication.ReplicationChannel; +import me.whereareiam.identica.replication.ReplicationSystem; +import me.whereareiam.identica.replication.cache.LocalCache; +import me.whereareiam.identica.replication.cache.ReplicatedCache; +import me.whereareiam.identica.replication.cache.base.ReplicationCacheBuilder; +import me.whereareiam.identica.replication.codec.SnapshotCodec; +import me.whereareiam.identica.replication.codec.SnapshotCodecFactory; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; + +import static org.junit.jupiter.api.Assertions.*; + +@DisplayName("Default Recognized Connection Store") +class DefaultRecognizedConnectionStoreTest { + @DisplayName("Uses configured namespace and TTL") + @Test + void usesConfiguredNamespaceAndTtl() { + TestReplicationSystem replicationSystem = new TestReplicationSystem(); + DefaultRecognizedConnectionStore store = new DefaultRecognizedConnectionStore( + replicationSystem, + () -> settings(Duration.ofSeconds(45), "recognition:test:connections"), + () -> replication(false) + ); + UUID connectionUniqueId = UUID.randomUUID(); + + store.markRecognized(connectionUniqueId); + + assertTrue(store.isRecognized(connectionUniqueId)); + assertTrue(store.consumeRecognized(connectionUniqueId)); + assertEquals("recognition:test:connections", replicationSystem.lastCacheName); + assertEquals(45_000L, replicationSystem.lastDefaultTtlMs); + assertEquals(45_000L, replicationSystem.lastWriteTtlMs); + } + + @DisplayName("Requires a configured recognized connection namespace") + @Test + void requiresConfiguredRecognizedConnectionNamespace() { + IllegalStateException exception = assertThrows(IllegalStateException.class, () -> + new DefaultRecognizedConnectionStore( + new TestReplicationSystem(), + () -> settings(Duration.ofSeconds(30), " "), + () -> replication(true) + )); + + assertTrue(exception.getMessage().contains("providers.capabilities.recognition.settings.replication.recognizedConnectionNamespace")); + } + + private @NotNull RecognitionSettings settings(@NotNull Duration ttl, @NotNull String namespace) { + RecognitionSettings settings = new RecognitionSettings(); + settings.setWindow(ttl); + + RecognitionSettings.Replication replication = new RecognitionSettings.Replication(); + replication.setRecognizedConnectionNamespace(namespace); + replication.setSnapshotNamespace("recognition:test:snapshots"); + settings.setReplication(replication); + return settings; + } + + private @NotNull Replication replication(boolean enabled) { + Replication replication = new Replication(); + replication.setEnabled(enabled); + return replication; + } + + private static final class TestReplicationSystem implements ReplicationSystem { + private final Map values = new ConcurrentHashMap<>(); + private SnapshotCodecFactory codecFactory = new SnapshotCodecFactory() { + @Override + public @NotNull SnapshotCodec codecFor(@NotNull Class snapshotType) { + return SnapshotCodec.json(snapshotType); + } + }; + + private String lastCacheName; + private long lastDefaultTtlMs; + private long lastWriteTtlMs; + + @Override + public @NotNull ReplicationCacheBuilder cache(@NotNull String name) { + lastCacheName = name; + + return new ReplicationCacheBuilder() { + private long defaultTtlMs; + + @Override + public @NotNull ReplicationCacheBuilder defaultTtl(long ttlMs) { + defaultTtlMs = ttlMs; + lastDefaultTtlMs = ttlMs; + return this; + } + + @Override + public @NotNull LocalCache local() { + return new TestCache<>(name, values, defaultTtlMs, TestReplicationSystem.this::recordWriteTtl); + } + + @Override + public @NotNull ReplicatedCache replicated(@NotNull ReplicationType type) { + return new TestCache<>(name, values, defaultTtlMs, TestReplicationSystem.this::recordWriteTtl); + } + }; + } + + @Override + public @NotNull ReplicationChannel channel( + @NotNull String name, + @NotNull ReplicationType type + ) { + throw new UnsupportedOperationException("channels are not used by this test"); + } + + @Override + public void setDefaultCodecFactory(@NotNull SnapshotCodecFactory factory) { + codecFactory = factory; + } + + @Override + public @NotNull SnapshotCodecFactory getDefaultCodecFactory() { + return codecFactory; + } + + private void recordWriteTtl(long ttlMs) { + lastWriteTtlMs = ttlMs; + } + } + + private static final class TestCache implements LocalCache, ReplicatedCache { + private final String namespace; + private final Map values; + private final long defaultTtlMs; + private final Consumer writeTtlConsumer; + + private TestCache( + @NotNull String namespace, + @NotNull Map values, + long defaultTtlMs, + @NotNull Consumer writeTtlConsumer + ) { + this.namespace = namespace; + this.values = values; + this.defaultTtlMs = defaultTtlMs; + this.writeTtlConsumer = writeTtlConsumer; + } + + @Override + public @NotNull CompletableFuture> get(String key) { + return CompletableFuture.completedFuture(Optional.ofNullable(value(key))); + } + + @Override + public @NotNull CompletableFuture put(String key, T value, long ttlMs) { + if (key != null && value != null) { + values.put(storageKey(key), value); + writeTtlConsumer.accept(ttlMs); + } + + return CompletableFuture.completedFuture(null); + } + + @Override + public long defaultTtlMs() { + return defaultTtlMs; + } + + @Override + public @NotNull CompletableFuture invalidate(String key) { + if (key != null) + values.remove(storageKey(key)); + + return CompletableFuture.completedFuture(null); + } + + private @SuppressWarnings("unchecked") T value(String key) { + if (key == null) return null; + return (T) values.get(storageKey(key)); + } + + private @NotNull String storageKey(@NotNull String key) { + return namespace + ":" + key; + } + } +} diff --git a/identica-capability/capability-recognition/recognition-common/src/test/java/me/whereareiam/identica/provider/capability/recognition/store/DefaultSessionRecognitionStoreTest.java b/identica-capability/capability-recognition/recognition-common/src/test/java/me/whereareiam/identica/provider/capability/recognition/store/DefaultSessionRecognitionStoreTest.java new file mode 100644 index 000000000..5a0e7c308 --- /dev/null +++ b/identica-capability/capability-recognition/recognition-common/src/test/java/me/whereareiam/identica/provider/capability/recognition/store/DefaultSessionRecognitionStoreTest.java @@ -0,0 +1,184 @@ +package me.whereareiam.identica.provider.capability.recognition.store; + +import me.whereareiam.identica.model.config.Replication; +import me.whereareiam.identica.model.replication.ReplicationType; +import me.whereareiam.identica.provider.capability.recognition.config.RecognitionSettings; +import me.whereareiam.identica.provider.capability.recognition.model.SessionRecognitionSnapshot; +import me.whereareiam.identica.replication.ReplicationChannel; +import me.whereareiam.identica.replication.ReplicationSystem; +import me.whereareiam.identica.replication.cache.LocalCache; +import me.whereareiam.identica.replication.cache.ReplicatedCache; +import me.whereareiam.identica.replication.cache.base.ReplicationCacheBuilder; +import me.whereareiam.identica.replication.codec.SnapshotCodec; +import me.whereareiam.identica.replication.codec.SnapshotCodecFactory; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; + +import static org.junit.jupiter.api.Assertions.*; + +@DisplayName("Default Session-Recognition Store") +class DefaultSessionRecognitionStoreTest { + @DisplayName("Recognition snapshots are keyed by provider id and subject") + @Test + void snapshotsUseProviderIdAndSubjectKey() { + DefaultSessionRecognitionStore store = new DefaultSessionRecognitionStore( + this::settings, + this::replication, + new TestReplicationSystem() + ); + + store.save(SessionRecognitionSnapshot.builder() + .providerId("premium") + .providerSubject("subject-1") + .providerUsername("PlayerOne") + .lastIp("127.0.0.1") + .capturedAt(System.currentTimeMillis()) + .build()); + + SessionRecognitionSnapshot found = store.find("premium", "subject-1").orElse(null); + assertNotNull(found); + assertEquals("PlayerOne", found.getProviderUsername()); + assertEquals("127.0.0.1", found.getLastIp()); + } + + @DisplayName("Recognition snapshots do not leak across subjects") + @Test + void snapshotsRequireMatchingSubject() { + DefaultSessionRecognitionStore store = new DefaultSessionRecognitionStore( + this::settings, + this::replication, + new TestReplicationSystem() + ); + + store.save(SessionRecognitionSnapshot.builder() + .providerId("premium") + .providerSubject("subject-1") + .providerUsername("PlayerOne") + .capturedAt(System.currentTimeMillis()) + .build()); + + assertTrue(store.find("premium", "subject-2").isEmpty()); + } + + private Replication replication() { + Replication replication = new Replication(); + replication.setEnabled(true); + return replication; + } + + private RecognitionSettings settings() { + RecognitionSettings settings = new RecognitionSettings(); + settings.setValidity(Duration.ofHours(12)); + RecognitionSettings.Replication replication = new RecognitionSettings.Replication(); + replication.setSnapshotNamespace("session-recognition:snapshot"); + settings.setReplication(replication); + return settings; + } + + private static final class TestReplicationSystem implements ReplicationSystem { + private final Map values = new ConcurrentHashMap<>(); + private SnapshotCodecFactory codecFactory = new SnapshotCodecFactory() { + @Override + public @NotNull SnapshotCodec codecFor(@NotNull Class snapshotType) { + return SnapshotCodec.json(snapshotType); + } + }; + + @Override + public @NotNull ReplicationCacheBuilder cache(@NotNull String name) { + return new ReplicationCacheBuilder() { + private long defaultTtlMs; + + @Override + public @NotNull ReplicationCacheBuilder defaultTtl(long ttlMs) { + this.defaultTtlMs = ttlMs; + return this; + } + + @Override + public @NotNull LocalCache local() { + throw new UnsupportedOperationException("local cache is not used by this test"); + } + + @Override + public @NotNull ReplicatedCache replicated(@NotNull ReplicationType type) { + return new TestReplicatedCache<>(name, values, defaultTtlMs); + } + }; + } + + @Override + public @NotNull ReplicationChannel channel( + @NotNull String name, + @NotNull ReplicationType type + ) { + throw new UnsupportedOperationException("channels are not used by this test"); + } + + @Override + public void setDefaultCodecFactory(@NotNull SnapshotCodecFactory factory) { + this.codecFactory = factory; + } + + @Override + public @NotNull SnapshotCodecFactory getDefaultCodecFactory() { + return codecFactory; + } + } + + private static final class TestReplicatedCache implements ReplicatedCache { + private final String namespace; + private final Map values; + private final long defaultTtlMs; + + private TestReplicatedCache(String namespace, Map values, long defaultTtlMs) { + this.namespace = namespace; + this.values = values; + this.defaultTtlMs = defaultTtlMs; + } + + @Override + public @NotNull CompletableFuture> get(@Nullable String key) { + return CompletableFuture.completedFuture(Optional.ofNullable(value(key))); + } + + @Override + public @NotNull CompletableFuture put(@Nullable String key, @Nullable T value, long ttlMs) { + if (key != null && value != null) + values.put(storageKey(key), value); + + return CompletableFuture.completedFuture(null); + } + + @Override + public long defaultTtlMs() { + return defaultTtlMs; + } + + @Override + public @NotNull CompletableFuture invalidate(@Nullable String key) { + if (key != null) + values.remove(storageKey(key)); + + return CompletableFuture.completedFuture(null); + } + + @SuppressWarnings("unchecked") + private @Nullable T value(@Nullable String key) { + if (key == null) return null; + return (T) values.get(storageKey(key)); + } + + private @NotNull String storageKey(@NotNull String key) { + return namespace + ":" + key; + } + } +} diff --git a/identica-capability/capability-recognition/recognition/build.gradle.kts b/identica-capability/capability-recognition/recognition/build.gradle.kts new file mode 100644 index 000000000..5b140820d --- /dev/null +++ b/identica-capability/capability-recognition/recognition/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + id("capability-artifact") +} + +dependencies { + api(projects.capabilityRecognitionApi) + implementation(projects.capabilityRecognitionCommon) +} + +toolkitPublish { + artifactId.set("recognition") +} diff --git a/identica-capability/capability-recognition/recognition/src/main/java/me/whereareiam/identica/provider/capability/recognition/bootstrap/RecognitionCapabilityBootstrap.java b/identica-capability/capability-recognition/recognition/src/main/java/me/whereareiam/identica/provider/capability/recognition/bootstrap/RecognitionCapabilityBootstrap.java new file mode 100644 index 000000000..f37416e39 --- /dev/null +++ b/identica-capability/capability-recognition/recognition/src/main/java/me/whereareiam/identica/provider/capability/recognition/bootstrap/RecognitionCapabilityBootstrap.java @@ -0,0 +1,90 @@ +package me.whereareiam.identica.provider.capability.recognition.bootstrap; + +import com.google.inject.ConfigurationException; +import com.google.inject.Injector; +import com.google.inject.Module; +import me.whereareiam.identica.event.EventManager; +import me.whereareiam.identica.model.provider.capability.ProviderCapabilityDeclaration; +import me.whereareiam.identica.pipeline.extension.PipelineExtensionRegistry; +import me.whereareiam.identica.provider.capability.bootstrap.ProviderCapabilityBootstrap; +import me.whereareiam.identica.provider.capability.bootstrap.ProviderCapabilityGlobalInstallContext; +import me.whereareiam.identica.provider.capability.bootstrap.ProviderCapabilityInitializationContext; +import me.whereareiam.identica.provider.capability.bootstrap.ProviderCapabilityLocalInstallContext; +import me.whereareiam.identica.provider.capability.recognition.RecognitionCapability; +import me.whereareiam.identica.provider.capability.recognition.RecognitionGlobalModule; +import me.whereareiam.identica.provider.capability.recognition.RecognitionLocalModule; +import me.whereareiam.identica.provider.capability.recognition.config.RecognitionSettings; +import me.whereareiam.identica.provider.capability.recognition.eligibility.RecognitionEligibilityRegistry; +import me.whereareiam.identica.provider.capability.recognition.eligibility.rule.ExplicitSelectionRecognitionEligibilityRule; +import me.whereareiam.identica.provider.capability.recognition.eligibility.rule.RecognitionEnabledEligibilityRule; +import me.whereareiam.identica.provider.capability.recognition.eligibility.rule.UntrustedIpRecognitionEligibilityRule; +import me.whereareiam.identica.provider.capability.recognition.pipeline.RecognitionAppliedLifecycle; +import me.whereareiam.identica.provider.capability.recognition.pipeline.RecognitionPipelineExtension; +import me.whereareiam.identica.provider.capability.restriction.join.JoinRestrictionType; +import me.whereareiam.identica.provider.capability.restriction.model.RestrictionSignalDescriptor; +import me.whereareiam.identica.provider.capability.restriction.registry.RestrictionSignalRegistry; +import me.whereareiam.identica.provider.capability.restriction.type.RestrictionSignal; +import me.whereareiam.identica.type.provider.capability.ProviderCapabilityScope; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Set; + +/** + * Bootstrap for the built-in reconnect recognition capability. + */ +public final class RecognitionCapabilityBootstrap implements ProviderCapabilityBootstrap { + public static final @NotNull RecognitionCapabilityBootstrap INSTANCE = new RecognitionCapabilityBootstrap(); + + @Override + public @NotNull ProviderCapabilityDeclaration declaration() { + return ProviderCapabilityDeclaration.builder() + .capability(RecognitionCapability.CAPABILITY) + .scopes(Set.of(ProviderCapabilityScope.GLOBAL, ProviderCapabilityScope.LOCAL)) + .build(); + } + + @Override + public void initialize(@NotNull ProviderCapabilityInitializationContext context) { + Injector globalInjector = context.getGlobalInjector(); + if (globalInjector == null) return; + + globalInjector.getInstance(RecognitionSettings.class); + + RecognitionEligibilityRegistry eligibilityRegistry = globalInjector.getInstance(RecognitionEligibilityRegistry.class); + eligibilityRegistry.register(globalInjector.getInstance(RecognitionEnabledEligibilityRule.class)); + eligibilityRegistry.register(globalInjector.getInstance(ExplicitSelectionRecognitionEligibilityRule.class)); + eligibilityRegistry.register(globalInjector.getInstance(UntrustedIpRecognitionEligibilityRule.class)); + + context.getRootInjector() + .getInstance(PipelineExtensionRegistry.class) + .register(globalInjector.getInstance(RecognitionPipelineExtension.class)); + context.getRootInjector() + .getInstance(EventManager.class) + .register(globalInjector.getInstance(RecognitionAppliedLifecycle.class)); + + try { + context.getRootInjector() + .getInstance(RestrictionSignalRegistry.class) + .register(RestrictionSignalDescriptor.builder() + .restrictionType(JoinRestrictionType.TYPE) + .signal(RestrictionSignal.of("recognized")) + .displayName("Recognized") + .description("Allows recognized reconnects through join restriction.") + .build()); + } catch (ConfigurationException ignored) { + } + } + + @Override + public @NotNull List globalModules(@NotNull ProviderCapabilityGlobalInstallContext context) { + return List.of(new RecognitionGlobalModule( + context.getCapabilitiesPath().resolve(RecognitionCapability.CAPABILITY.getId()) + )); + } + + @Override + public @NotNull List localModules(@NotNull ProviderCapabilityLocalInstallContext context) { + return List.of(new RecognitionLocalModule()); + } +} diff --git a/identica-capability/capability-restriction/build.gradle.kts b/identica-capability/capability-restriction/build.gradle.kts new file mode 100644 index 000000000..6c8344122 --- /dev/null +++ b/identica-capability/capability-restriction/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + id("base") +} diff --git a/identica-capability/capability-restriction/restriction-api/build.gradle.kts b/identica-capability/capability-restriction/restriction-api/build.gradle.kts new file mode 100644 index 000000000..6fc0791bd --- /dev/null +++ b/identica-capability/capability-restriction/restriction-api/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + id("capability") +} + +toolkitPublish { + artifactId.set("restriction-api") +} diff --git a/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/RestrictionCapability.java b/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/RestrictionCapability.java new file mode 100644 index 000000000..ce23cbd40 --- /dev/null +++ b/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/RestrictionCapability.java @@ -0,0 +1,14 @@ +package me.whereareiam.identica.provider.capability.restriction; + +import me.whereareiam.identica.type.provider.capability.ProviderCapability; +import org.jetbrains.annotations.NotNull; + +/** + * Built-in restriction capability identifier. + */ +public final class RestrictionCapability { + /** + * Generic restriction capability id. + */ + public static final @NotNull ProviderCapability CAPABILITY = ProviderCapability.of("restriction"); +} diff --git a/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/RestrictionService.java b/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/RestrictionService.java new file mode 100644 index 000000000..dbc0255c7 --- /dev/null +++ b/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/RestrictionService.java @@ -0,0 +1,59 @@ +package me.whereareiam.identica.provider.capability.restriction; + +import me.whereareiam.identica.provider.capability.restriction.model.RestrictionDecision; +import me.whereareiam.identica.provider.capability.restriction.model.RestrictionEvaluationRequest; +import me.whereareiam.identica.provider.capability.restriction.model.RestrictionStatus; +import me.whereareiam.identica.provider.capability.restriction.type.RestrictionType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Optional; + +/** + * Runtime manager and policy service for capability-owned restrictions. + */ +public interface RestrictionService { + /** + * Enables the supplied restriction type for a provider. + * + * @param type restriction type + * @param providerId provider id + * @return resolved status after enabling + */ + @NotNull RestrictionStatus enable(@NotNull RestrictionType type, @Nullable String providerId); + + /** + * Disables the supplied restriction type for a provider. + * + * @param type restriction type + * @param providerId provider id + * @return resolved status after disabling + */ + @NotNull RestrictionStatus disable(@NotNull RestrictionType type, @Nullable String providerId); + + /** + * Returns status for a single provider restriction. + * + * @param type restriction type + * @param providerId provider id + * @return status or empty when the provider/type pair is unknown + */ + @NotNull Optional status(@NotNull RestrictionType type, @Nullable String providerId); + + /** + * Returns statuses for all providers supporting the supplied restriction type. + * + * @param type restriction type + * @return resolved statuses + */ + @NotNull List statuses(@NotNull RestrictionType type); + + /** + * Evaluates a restriction request. + * + * @param request evaluation request + * @return resolved decision + */ + @NotNull RestrictionDecision evaluate(@NotNull RestrictionEvaluationRequest request); +} diff --git a/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/RestrictionTypeResolver.java b/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/RestrictionTypeResolver.java new file mode 100644 index 000000000..536be8e63 --- /dev/null +++ b/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/RestrictionTypeResolver.java @@ -0,0 +1,36 @@ +package me.whereareiam.identica.provider.capability.restriction; + +import me.whereareiam.identica.provider.capability.restriction.model.RestrictionTypeState; +import me.whereareiam.identica.provider.capability.restriction.type.RestrictionType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Optional; + +/** + * Extension point that resolves configured state for a restriction type. + */ +public interface RestrictionTypeResolver { + /** + * Returns the restriction type handled by this resolver. + * + * @return handled restriction type + */ + @NotNull RestrictionType type(); + + /** + * Resolves state for a single provider. + * + * @param providerId provider id + * @return resolved state or empty when the provider is unknown for this type + */ + @NotNull Optional resolve(@Nullable String providerId); + + /** + * Resolves state for every provider supported by this type. + * + * @return resolved provider states + */ + @NotNull List resolveAll(); +} diff --git a/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/contribution/ProviderRestrictionSignalContribution.java b/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/contribution/ProviderRestrictionSignalContribution.java new file mode 100644 index 000000000..130f0b582 --- /dev/null +++ b/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/contribution/ProviderRestrictionSignalContribution.java @@ -0,0 +1,35 @@ +package me.whereareiam.identica.provider.capability.restriction.contribution; + +import me.whereareiam.identica.provider.capability.contribution.ProviderCapabilityContribution; +import me.whereareiam.identica.provider.capability.restriction.model.RestrictionEvaluationRequest; +import me.whereareiam.identica.provider.capability.restriction.type.RestrictionSignal; +import me.whereareiam.identica.provider.capability.restriction.type.RestrictionType; +import org.jetbrains.annotations.NotNull; + +/** + * Provider-local matcher for a restriction signal owned by a capability or + * provider module. + */ +public interface ProviderRestrictionSignalContribution extends ProviderCapabilityContribution { + /** + * Returns the restriction type this contribution supports. + * + * @return supported restriction type + */ + @NotNull RestrictionType restrictionType(); + + /** + * Returns the signal contributed for the restriction type. + * + * @return contributed signal id + */ + @NotNull RestrictionSignal signal(); + + /** + * Determines whether the signal matches the supplied request. + * + * @param request evaluation request + * @return {@code true} when the signal matches + */ + boolean matches(@NotNull RestrictionEvaluationRequest request); +} diff --git a/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/model/RestrictionDecision.java b/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/model/RestrictionDecision.java new file mode 100644 index 000000000..554c4b6f3 --- /dev/null +++ b/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/model/RestrictionDecision.java @@ -0,0 +1,61 @@ +package me.whereareiam.identica.provider.capability.restriction.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; +import me.whereareiam.identica.provider.capability.restriction.type.RestrictionSignal; +import me.whereareiam.identica.provider.capability.restriction.type.RestrictionType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Set; + +/** + * Resolved restriction decision for a provider restriction evaluation. + */ +@Getter +@ToString +@AllArgsConstructor +@Builder(toBuilder = true) +public class RestrictionDecision { + private final @NotNull RestrictionType type; + private final @Nullable String providerId; + private final boolean allowed; + private final boolean configured; + private final boolean active; + private final @NotNull Set allow; + private final @NotNull Set matchedSignals; + + /** + * Creates a resolved restriction decision for a provider evaluation. + * + * @param type restriction type + * @param providerId provider id + * @param allowed whether the evaluation allows the operation + * @param configured whether the restriction is configured for the provider + * @param active whether the restriction is currently active + * @param allow allowed signals configured for the restriction + * @param matchedSignals signals that matched during evaluation + * @return resolved restriction decision + */ + public static @NotNull RestrictionDecision of( + @NotNull RestrictionType type, + @Nullable String providerId, + boolean allowed, + boolean configured, + boolean active, + @NotNull Set allow, + @NotNull Set matchedSignals + ) { + return RestrictionDecision.builder() + .type(type) + .providerId(providerId) + .allowed(allowed) + .configured(configured) + .active(active) + .allow(allow) + .matchedSignals(matchedSignals) + .build(); + } +} diff --git a/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/model/RestrictionEvaluationRequest.java b/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/model/RestrictionEvaluationRequest.java new file mode 100644 index 000000000..2387e568a --- /dev/null +++ b/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/model/RestrictionEvaluationRequest.java @@ -0,0 +1,27 @@ +package me.whereareiam.identica.provider.capability.restriction.model; + +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; +import me.whereareiam.identica.identity.actor.ConnectionIdentity; +import me.whereareiam.identica.provider.capability.restriction.type.RestrictionType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.UUID; + +/** + * Input used to evaluate a provider restriction type. + */ +@Getter +@ToString +@Builder(toBuilder = true) +public class RestrictionEvaluationRequest { + private final @NotNull RestrictionType type; + private final @Nullable String providerId; + private final @Nullable String providerSubject; + private final @Nullable String providerUsername; + private final @Nullable UUID connectionUniqueId; + private final @Nullable String ip; + private final @Nullable ConnectionIdentity.Origin origin; +} diff --git a/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/model/RestrictionSignalDescriptor.java b/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/model/RestrictionSignalDescriptor.java new file mode 100644 index 000000000..3d9f74ad4 --- /dev/null +++ b/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/model/RestrictionSignalDescriptor.java @@ -0,0 +1,22 @@ +package me.whereareiam.identica.provider.capability.restriction.model; + +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; +import me.whereareiam.identica.provider.capability.restriction.type.RestrictionSignal; +import me.whereareiam.identica.provider.capability.restriction.type.RestrictionType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Metadata describing a globally known restriction signal. + */ +@Getter +@ToString +@Builder +public final class RestrictionSignalDescriptor { + private final @NotNull RestrictionType restrictionType; + private final @NotNull RestrictionSignal signal; + private final @Nullable String displayName; + private final @Nullable String description; +} diff --git a/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/model/RestrictionStatus.java b/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/model/RestrictionStatus.java new file mode 100644 index 000000000..4ae12aea2 --- /dev/null +++ b/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/model/RestrictionStatus.java @@ -0,0 +1,53 @@ +package me.whereareiam.identica.provider.capability.restriction.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; +import me.whereareiam.identica.provider.capability.restriction.type.RestrictionSignal; +import me.whereareiam.identica.provider.capability.restriction.type.RestrictionType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Set; + +/** + * Resolved status of a provider restriction type. + */ +@Getter +@ToString +@AllArgsConstructor +@Builder(toBuilder = true) +public class RestrictionStatus { + private final @NotNull RestrictionType type; + private final @Nullable String providerId; + private final boolean active; + private final boolean configured; + private final @NotNull Set allow; + + /** + * Creates a resolved status for a provider restriction type. + * + * @param type restriction type + * @param providerId provider id + * @param active whether the restriction is currently active + * @param configured whether the restriction is configured for the provider + * @param allow allowed signals configured for the restriction + * @return resolved restriction status + */ + public static @NotNull RestrictionStatus of( + @NotNull RestrictionType type, + @Nullable String providerId, + boolean active, + boolean configured, + @NotNull Set allow + ) { + return RestrictionStatus.builder() + .type(type) + .providerId(providerId) + .active(active) + .configured(configured) + .allow(allow) + .build(); + } +} diff --git a/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/model/RestrictionTypeDescriptor.java b/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/model/RestrictionTypeDescriptor.java new file mode 100644 index 000000000..202f774d3 --- /dev/null +++ b/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/model/RestrictionTypeDescriptor.java @@ -0,0 +1,20 @@ +package me.whereareiam.identica.provider.capability.restriction.model; + +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; +import me.whereareiam.identica.provider.capability.restriction.type.RestrictionType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Metadata describing a globally known restriction type. + */ +@Getter +@ToString +@Builder +public final class RestrictionTypeDescriptor { + private final @NotNull RestrictionType type; + private final @Nullable String displayName; + private final @Nullable String description; +} diff --git a/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/model/RestrictionTypeState.java b/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/model/RestrictionTypeState.java new file mode 100644 index 000000000..33caf4148 --- /dev/null +++ b/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/model/RestrictionTypeState.java @@ -0,0 +1,24 @@ +package me.whereareiam.identica.provider.capability.restriction.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; +import me.whereareiam.identica.provider.capability.restriction.type.RestrictionSignal; +import org.jetbrains.annotations.NotNull; + +import java.util.Set; + +/** + * Resolved provider state for a restriction type. + */ +@Getter +@ToString +@AllArgsConstructor +@Builder(toBuilder = true) +public class RestrictionTypeState { + private final @NotNull String providerId; + private final boolean enabled; + private final boolean configured; + private final @NotNull Set allow; +} diff --git a/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/registry/RestrictionSignalRegistry.java b/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/registry/RestrictionSignalRegistry.java new file mode 100644 index 000000000..cbe16e2d3 --- /dev/null +++ b/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/registry/RestrictionSignalRegistry.java @@ -0,0 +1,44 @@ +package me.whereareiam.identica.provider.capability.restriction.registry; + +import me.whereareiam.identica.provider.capability.restriction.model.RestrictionSignalDescriptor; +import me.whereareiam.identica.provider.capability.restriction.type.RestrictionType; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Optional; + +/** + * Registry of globally known restriction signals. + */ +public interface RestrictionSignalRegistry { + /** + * Registers signal metadata. + * + * @param descriptor signal descriptor + */ + void register(@NotNull RestrictionSignalDescriptor descriptor); + + /** + * Unregisters signal metadata. + * + * @param descriptor signal descriptor + */ + void unregister(@NotNull RestrictionSignalDescriptor descriptor); + + /** + * Resolves a signal descriptor for the supplied type and id. + * + * @param type restriction type + * @param signalId signal id + * @return resolved descriptor or empty when missing + */ + @NotNull Optional find(@NotNull RestrictionType type, @NotNull String signalId); + + /** + * Returns the globally declared signals for the supplied type. + * + * @param type restriction type + * @return signal descriptors + */ + @NotNull List signals(@NotNull RestrictionType type); +} diff --git a/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/registry/type/RestrictionTypeRegistry.java b/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/registry/type/RestrictionTypeRegistry.java new file mode 100644 index 000000000..928eace81 --- /dev/null +++ b/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/registry/type/RestrictionTypeRegistry.java @@ -0,0 +1,42 @@ +package me.whereareiam.identica.provider.capability.restriction.registry.type; + +import me.whereareiam.identica.provider.capability.restriction.model.RestrictionTypeDescriptor; +import me.whereareiam.identica.provider.capability.restriction.type.RestrictionType; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Optional; + +/** + * Registry of globally known restriction types. + */ +public interface RestrictionTypeRegistry { + /** + * Registers a restriction type descriptor. + * + * @param descriptor type descriptor + */ + void register(@NotNull RestrictionTypeDescriptor descriptor); + + /** + * Unregisters a restriction type descriptor. + * + * @param descriptor type descriptor + */ + void unregister(@NotNull RestrictionTypeDescriptor descriptor); + + /** + * Resolves descriptor metadata for the supplied type. + * + * @param type restriction type + * @return resolved descriptor or empty when missing + */ + @NotNull Optional find(@NotNull RestrictionType type); + + /** + * Returns all declared type descriptors. + * + * @return registered descriptors + */ + @NotNull List values(); +} diff --git a/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/registry/type/RestrictionTypeResolverRegistry.java b/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/registry/type/RestrictionTypeResolverRegistry.java new file mode 100644 index 000000000..fd221b220 --- /dev/null +++ b/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/registry/type/RestrictionTypeResolverRegistry.java @@ -0,0 +1,41 @@ +package me.whereareiam.identica.provider.capability.restriction.registry.type; + +import me.whereareiam.identica.provider.capability.restriction.RestrictionTypeResolver; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Optional; + +/** + * Registry of available restriction type handlers. + */ +public interface RestrictionTypeResolverRegistry { + /** + * Registers a handler for a restriction type. + * + * @param handler handler to register + */ + void register(@NotNull RestrictionTypeResolver handler); + + /** + * Unregisters a handler for a restriction type. + * + * @param handler handler to unregister + */ + void unregister(@NotNull RestrictionTypeResolver handler); + + /** + * Finds the handler for a restriction type id. + * + * @param typeId restriction type id + * @return resolved handler or empty when missing + */ + @NotNull Optional find(@NotNull String typeId); + + /** + * Returns all registered handlers. + * + * @return registered handlers + */ + @NotNull List values(); +} diff --git a/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/type/RestrictionSignal.java b/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/type/RestrictionSignal.java new file mode 100644 index 000000000..186549c72 --- /dev/null +++ b/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/type/RestrictionSignal.java @@ -0,0 +1,66 @@ +package me.whereareiam.identica.provider.capability.restriction.type; + +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Locale; +import java.util.Objects; + +/** + * Normalized identifier for a restriction signal. + */ +@RequiredArgsConstructor +public final class RestrictionSignal { + private final @NotNull String id; + + /** + * Creates a normalized restriction signal id. + * + * @param id raw signal id + * @return normalized signal id + */ + public static @NotNull RestrictionSignal of(@Nullable String id) { + String normalized = id == null ? "" : id.trim().toLowerCase(Locale.ROOT); + if (normalized.isBlank()) + throw new IllegalArgumentException("Restriction signal id must not be blank"); + + return new RestrictionSignal(normalized); + } + + /** + * Returns the normalized signal id. + * + * @return normalized signal id + */ + public @NotNull String getId() { + return id; + } + + /** + * Returns whether the supplied raw value matches this signal id. + * + * @param value raw id value + * @return {@code true} when the value matches + */ + public boolean matches(@Nullable String value) { + return value != null && id.equals(value.trim().toLowerCase(Locale.ROOT)); + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + if (!(other instanceof RestrictionSignal that)) return false; + return id.equals(that.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public @NotNull String toString() { + return id; + } +} diff --git a/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/type/RestrictionType.java b/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/type/RestrictionType.java new file mode 100644 index 000000000..66753263f --- /dev/null +++ b/identica-capability/capability-restriction/restriction-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/type/RestrictionType.java @@ -0,0 +1,66 @@ +package me.whereareiam.identica.provider.capability.restriction.type; + +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Locale; +import java.util.Objects; + +/** + * Normalized identifier for a restriction type. + */ +@RequiredArgsConstructor +public final class RestrictionType { + private final @NotNull String id; + + /** + * Creates a normalized restriction type id. + * + * @param id raw type id + * @return normalized type id + */ + public static @NotNull RestrictionType of(@Nullable String id) { + String normalized = id == null ? "" : id.trim().toLowerCase(Locale.ROOT); + if (normalized.isBlank()) + throw new IllegalArgumentException("Restriction type id must not be blank"); + + return new RestrictionType(normalized); + } + + /** + * Returns the normalized type id. + * + * @return normalized type id + */ + public @NotNull String getId() { + return id; + } + + /** + * Returns whether the supplied raw value matches this type id. + * + * @param value raw id value + * @return {@code true} when the value matches + */ + public boolean matches(@Nullable String value) { + return value != null && id.equals(value.trim().toLowerCase(Locale.ROOT)); + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + if (!(other instanceof RestrictionType that)) return false; + return id.equals(that.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public @NotNull String toString() { + return id; + } +} diff --git a/identica-capability/capability-restriction/restriction-common/build.gradle.kts b/identica-capability/capability-restriction/restriction-common/build.gradle.kts new file mode 100644 index 000000000..499e6b119 --- /dev/null +++ b/identica-capability/capability-restriction/restriction-common/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + id("capability") +} + +dependencies { + api(projects.capabilityRestrictionApi) +} + +toolkitPublish { + artifactId.set("restriction-common") +} diff --git a/identica-capability/capability-restriction/restriction-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/DefaultRestrictionService.java b/identica-capability/capability-restriction/restriction-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/DefaultRestrictionService.java new file mode 100644 index 000000000..cc3958ac8 --- /dev/null +++ b/identica-capability/capability-restriction/restriction-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/DefaultRestrictionService.java @@ -0,0 +1,138 @@ +package me.whereareiam.identica.provider.capability.restriction; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import me.whereareiam.identica.model.provider.InternalProvider; +import me.whereareiam.identica.provider.ProviderManager; +import me.whereareiam.identica.provider.capability.contribution.ProviderCapabilityContribution; +import me.whereareiam.identica.provider.capability.restriction.contribution.ProviderRestrictionSignalContribution; +import me.whereareiam.identica.provider.capability.restriction.model.RestrictionDecision; +import me.whereareiam.identica.provider.capability.restriction.model.RestrictionEvaluationRequest; +import me.whereareiam.identica.provider.capability.restriction.model.RestrictionStatus; +import me.whereareiam.identica.provider.capability.restriction.model.RestrictionTypeState; +import me.whereareiam.identica.provider.capability.restriction.registry.type.RestrictionTypeResolverRegistry; +import me.whereareiam.identica.provider.capability.restriction.type.RestrictionSignal; +import me.whereareiam.identica.provider.capability.restriction.type.RestrictionType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +@Singleton +public class DefaultRestrictionService implements RestrictionService { + private final RestrictionTypeResolverRegistry handlerRegistry; + private final RestrictionActivationStore toggleStore; + private final ProviderManager providerManager; + + @Inject + public DefaultRestrictionService( + RestrictionTypeResolverRegistry handlerRegistry, + RestrictionActivationStore toggleStore, + ProviderManager providerManager + ) { + this.handlerRegistry = handlerRegistry; + this.toggleStore = toggleStore; + this.providerManager = providerManager; + } + + @Override + public @NotNull RestrictionStatus enable(@NotNull RestrictionType type, @Nullable String providerId) { + RestrictionTypeState state = resolveState(type, providerId).orElseGet(() -> missing(type, providerId)); + if (!state.isEnabled() || !state.isConfigured()) + return RestrictionStatus.of(type, state.getProviderId(), false, state.isConfigured(), state.getAllow()); + + toggleStore.enable(type, state.getProviderId()); + return RestrictionStatus.of(type, state.getProviderId(), true, true, state.getAllow()); + } + + @Override + public @NotNull RestrictionStatus disable(@NotNull RestrictionType type, @Nullable String providerId) { + RestrictionTypeState state = resolveState(type, providerId).orElseGet(() -> missing(type, providerId)); + toggleStore.disable(type, state.getProviderId()); + return RestrictionStatus.of(type, state.getProviderId(), false, state.isConfigured(), state.getAllow()); + } + + @Override + public @NotNull Optional status(@NotNull RestrictionType type, @Nullable String providerId) { + return resolveState(type, providerId) + .map(state -> RestrictionStatus.of(type, state.getProviderId(), isActive(type, state), state.isConfigured(), state.getAllow())); + } + + @Override + public @NotNull List statuses(@NotNull RestrictionType type) { + RestrictionTypeResolver handler = handlerRegistry.find(type.getId()).orElse(null); + if (handler == null) return List.of(); + + List statuses = new ArrayList<>(); + for (RestrictionTypeState state : handler.resolveAll()) + statuses.add(RestrictionStatus.of(type, state.getProviderId(), isActive(type, state), state.isConfigured(), state.getAllow())); + + return List.copyOf(statuses); + } + + @Override + public @NotNull RestrictionDecision evaluate(@NotNull RestrictionEvaluationRequest request) { + RestrictionType type = request.getType(); + RestrictionTypeState state = resolveState(type, request.getProviderId()).orElseGet(() -> missing(type, request.getProviderId())); + if (!isActive(type, state)) + return RestrictionDecision.of(type, state.getProviderId(), true, state.isConfigured(), false, state.getAllow(), Set.of()); + if (state.getAllow().isEmpty()) + return RestrictionDecision.of(type, state.getProviderId(), false, true, true, state.getAllow(), Set.of()); + + Set matchedSignals = new LinkedHashSet<>(); + for (ProviderRestrictionSignalContribution contribution : resolveSignalContributions(request.getProviderId(), type)) { + RestrictionSignal signal = contribution.signal(); + if (!state.getAllow().contains(signal)) continue; + if (contribution.matches(request)) matchedSignals.add(signal); + } + + if (!matchedSignals.isEmpty()) + return RestrictionDecision.of(type, state.getProviderId(), true, true, true, state.getAllow(), Set.copyOf(matchedSignals)); + + return RestrictionDecision.of(type, state.getProviderId(), false, true, true, state.getAllow(), Set.of()); + } + + private @NotNull Optional resolveState(@NotNull RestrictionType type, @Nullable String providerId) { + RestrictionTypeResolver handler = handlerRegistry.find(type.getId()).orElse(null); + if (handler == null) return Optional.empty(); + + return handler.resolve(providerId); + } + + private boolean isActive(@NotNull RestrictionType type, @NotNull RestrictionTypeState state) { + return state.isEnabled() && state.isConfigured() && toggleStore.isActive(type, state.getProviderId()); + } + + private @NotNull List resolveSignalContributions( + @Nullable String providerId, + @NotNull RestrictionType type + ) { + InternalProvider provider = findProvider(providerId); + if (provider == null || provider.getCapabilityContributions() == null) return List.of(); + + List resolved = new ArrayList<>(); + for (ProviderCapabilityContribution contribution : provider.getCapabilityContributions()) { + if (!(contribution instanceof ProviderRestrictionSignalContribution signalContribution)) continue; + if (!signalContribution.restrictionType().equals(type)) continue; + + resolved.add(signalContribution); + } + + return List.copyOf(resolved); + } + + private @Nullable InternalProvider findProvider(@Nullable String providerId) { + if (providerId == null || providerId.isBlank()) return null; + for (InternalProvider provider : providerManager.getProviders()) { + if (provider == null || provider.getDescriptor() == null) continue; + if (provider.getDescriptor().getId().equalsIgnoreCase(providerId)) + return provider; + } + return null; + } + + private @NotNull RestrictionTypeState missing(@NotNull RestrictionType type, @Nullable String providerId) { + String resolvedProviderId = providerId != null && !providerId.isBlank() ? providerId : ""; + return new RestrictionTypeState(resolvedProviderId, false, false, Set.of()); + } +} diff --git a/identica-capability/capability-restriction/restriction-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/DefaultRestrictionTypeResolverRegistry.java b/identica-capability/capability-restriction/restriction-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/DefaultRestrictionTypeResolverRegistry.java new file mode 100644 index 000000000..1e74dd298 --- /dev/null +++ b/identica-capability/capability-restriction/restriction-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/DefaultRestrictionTypeResolverRegistry.java @@ -0,0 +1,41 @@ +package me.whereareiam.identica.provider.capability.restriction; + +import com.google.inject.Singleton; +import me.whereareiam.identica.provider.capability.restriction.registry.type.RestrictionTypeResolverRegistry; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CopyOnWriteArrayList; + +@Singleton +public class DefaultRestrictionTypeResolverRegistry implements RestrictionTypeResolverRegistry { + private final List handlers = new CopyOnWriteArrayList<>(); + + @Override + public void register(@NotNull RestrictionTypeResolver handler) { + handlers.removeIf(existing -> existing.type().equals(handler.type())); + handlers.add(handler); + } + + @Override + public void unregister(@NotNull RestrictionTypeResolver handler) { + handlers.removeIf(existing -> existing.type().equals(handler.type())); + } + + @Override + public @NotNull Optional find(@NotNull String typeId) { + return handlers.stream() + .filter(handler -> handler.type().matches(typeId)) + .findFirst(); + } + + @Override + public @NotNull List values() { + List resolved = new ArrayList<>(handlers); + resolved.sort(Comparator.comparing(handler -> handler.type().getId(), String.CASE_INSENSITIVE_ORDER)); + return List.copyOf(resolved); + } +} diff --git a/identica-capability/capability-restriction/restriction-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/RestrictionActivationStore.java b/identica-capability/capability-restriction/restriction-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/RestrictionActivationStore.java new file mode 100644 index 000000000..2ce762b9f --- /dev/null +++ b/identica-capability/capability-restriction/restriction-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/RestrictionActivationStore.java @@ -0,0 +1,66 @@ +package me.whereareiam.identica.provider.capability.restriction; + +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import me.whereareiam.identica.model.replication.ReplicationType; +import me.whereareiam.identica.provider.capability.restriction.config.RestrictionSettings; +import me.whereareiam.identica.provider.capability.restriction.type.RestrictionType; +import me.whereareiam.identica.replication.ReplicationSystem; +import me.whereareiam.identica.replication.cache.ReplicatedCache; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Locale; + +@Singleton +public class RestrictionActivationStore { + private final Provider settingsProvider; + private final ReplicatedCache cache; + + @Inject + public RestrictionActivationStore( + @NotNull ReplicationSystem replicationSystem, + @NotNull Provider settingsProvider + ) { + this.settingsProvider = settingsProvider; + ReplicationType type = ReplicationType.identity(Boolean.class); + this.cache = replicationSystem.cache(resolveNamespace(settingsProvider)) + .replicated(type); + } + + public boolean isActive(@NotNull RestrictionType type, @Nullable String providerId) { + String key = key(type, providerId); + if (key == null) return false; + + return cache.get(key).join().orElse(false); + } + + public void enable(@NotNull RestrictionType type, @Nullable String providerId) { + String key = key(type, providerId); + if (key == null) return; + + cache.put(key, Boolean.TRUE, settingsProvider.get().toggleTtlMillis()).join(); + } + + public void disable(@NotNull RestrictionType type, @Nullable String providerId) { + String key = key(type, providerId); + if (key == null) return; + + cache.invalidate(key).join(); + } + + private @Nullable String key(@NotNull RestrictionType type, @Nullable String providerId) { + if (providerId == null || providerId.isBlank()) return null; + return type.getId() + ":" + providerId.trim().toLowerCase(Locale.ROOT); + } + + private static @NotNull String resolveNamespace(@NotNull Provider settingsProvider) { + String namespace = settingsProvider.get().getReplication().getToggleNamespace(); + if (namespace.isBlank()) { + throw new IllegalStateException("providers.capabilities.restriction.settings.replication.toggleNamespace is missing"); + } + + return namespace; + } +} diff --git a/identica-capability/capability-restriction/restriction-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/RestrictionGlobalModule.java b/identica-capability/capability-restriction/restriction-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/RestrictionGlobalModule.java new file mode 100644 index 000000000..ed5e2b9f7 --- /dev/null +++ b/identica-capability/capability-restriction/restriction-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/RestrictionGlobalModule.java @@ -0,0 +1,39 @@ +package me.whereareiam.identica.provider.capability.restriction; + +import com.google.inject.AbstractModule; +import com.google.inject.Provides; +import com.google.inject.Singleton; +import com.google.inject.name.Named; +import lombok.RequiredArgsConstructor; +import me.whereareiam.identica.provider.capability.restriction.config.RestrictionSettings; +import me.whereareiam.identica.provider.capability.restriction.config.provider.RestrictionSettingsProvider; +import me.whereareiam.identica.provider.capability.restriction.registry.DefaultRestrictionSignalRegistry; +import me.whereareiam.identica.provider.capability.restriction.registry.DefaultRestrictionTypeRegistry; +import me.whereareiam.identica.provider.capability.restriction.registry.RestrictionSignalRegistry; +import me.whereareiam.identica.provider.capability.restriction.registry.type.RestrictionTypeRegistry; +import me.whereareiam.identica.provider.capability.restriction.registry.type.RestrictionTypeResolverRegistry; + +import java.nio.file.Path; + +@RequiredArgsConstructor +public class RestrictionGlobalModule extends AbstractModule { + private final Path restrictionCapabilityPath; + + @Override + protected void configure() { + bind(RestrictionSettingsProvider.class).asEagerSingleton(); + bind(RestrictionSettings.class).toProvider(RestrictionSettingsProvider.class); + bind(RestrictionTypeRegistry.class).to(DefaultRestrictionTypeRegistry.class).asEagerSingleton(); + bind(RestrictionSignalRegistry.class).to(DefaultRestrictionSignalRegistry.class).asEagerSingleton(); + bind(RestrictionTypeResolverRegistry.class).to(DefaultRestrictionTypeResolverRegistry.class).asEagerSingleton(); + bind(RestrictionActivationStore.class).asEagerSingleton(); + bind(RestrictionService.class).to(DefaultRestrictionService.class).asEagerSingleton(); + } + + @Provides + @Singleton + @Named("restrictionCapabilityPath") + Path provideRestrictionCapabilityPath() { + return restrictionCapabilityPath; + } +} diff --git a/identica-capability/capability-restriction/restriction-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/config/RestrictionSettings.java b/identica-capability/capability-restriction/restriction-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/config/RestrictionSettings.java new file mode 100644 index 000000000..043591b8e --- /dev/null +++ b/identica-capability/capability-restriction/restriction-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/config/RestrictionSettings.java @@ -0,0 +1,39 @@ +package me.whereareiam.identica.provider.capability.restriction.config; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import me.whereareiam.configura.ConfigDocument; +import org.jetbrains.annotations.NotNull; + +import java.time.Duration; + +/** + * Shared runtime settings for the restriction capability. + */ +@Getter +@Setter +@ToString +public class RestrictionSettings extends ConfigDocument { + private @NotNull Duration toggleTtl; + private @NotNull Replication replication; + + /** + * Returns toggle TTL in milliseconds with validation. + * + * @return toggle TTL in milliseconds + */ + public long toggleTtlMillis() { + if (toggleTtl.isZero() || toggleTtl.isNegative()) + throw new IllegalStateException("providers.capabilities.restriction.settings.toggleTtl must be positive"); + + return toggleTtl.toMillis(); + } + + @Getter + @Setter + @ToString + public static class Replication { + private @NotNull String toggleNamespace; + } +} diff --git a/identica-capability/capability-restriction/restriction-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/config/defaults/RestrictionSettingsDefaults.java b/identica-capability/capability-restriction/restriction-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/config/defaults/RestrictionSettingsDefaults.java new file mode 100644 index 000000000..a72a8debd --- /dev/null +++ b/identica-capability/capability-restriction/restriction-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/config/defaults/RestrictionSettingsDefaults.java @@ -0,0 +1,20 @@ +package me.whereareiam.identica.provider.capability.restriction.config.defaults; + +import com.google.inject.Singleton; +import me.whereareiam.configura.merge.defaults.DefaultsProvider; +import me.whereareiam.identica.provider.capability.restriction.config.RestrictionSettings; + +import java.time.Duration; + +@Singleton +public class RestrictionSettingsDefaults implements DefaultsProvider { + @Override + public RestrictionSettings supply(RestrictionSettings settings) { + settings.setToggleTtl(Duration.ofDays(365)); + + RestrictionSettings.Replication replication = new RestrictionSettings.Replication(); + replication.setToggleNamespace("identica:restriction:toggle"); + settings.setReplication(replication); + return settings; + } +} diff --git a/identica-capability/capability-restriction/restriction-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/config/provider/RestrictionSettingsProvider.java b/identica-capability/capability-restriction/restriction-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/config/provider/RestrictionSettingsProvider.java new file mode 100644 index 000000000..462077891 --- /dev/null +++ b/identica-capability/capability-restriction/restriction-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/config/provider/RestrictionSettingsProvider.java @@ -0,0 +1,30 @@ +package me.whereareiam.identica.provider.capability.restriction.config.provider; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.google.inject.name.Named; +import me.whereareiam.configura.Config; +import me.whereareiam.configura.Configura; +import me.whereareiam.identica.Registry; +import me.whereareiam.identica.Reloadable; +import me.whereareiam.identica.config.ConfigProvider; +import me.whereareiam.identica.provider.capability.restriction.config.RestrictionSettings; +import me.whereareiam.identica.provider.capability.restriction.config.defaults.RestrictionSettingsDefaults; + +import java.nio.file.Path; + +@Singleton +public class RestrictionSettingsProvider extends ConfigProvider { + @Inject + public RestrictionSettingsProvider( + @Named("restrictionCapabilityPath") Path restrictionCapabilityPath, + Registry reloadables + ) { + super(restrictionCapabilityPath, "settings", RestrictionSettings.class, reloadables); + } + + @Override + protected Configura configura() { + return versioned(Config.configured().withDefaults(RestrictionSettingsDefaults.class), RestrictionSettings.class); + } +} diff --git a/identica-capability/capability-restriction/restriction-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/registry/DefaultRestrictionSignalRegistry.java b/identica-capability/capability-restriction/restriction-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/registry/DefaultRestrictionSignalRegistry.java new file mode 100644 index 000000000..982a409e1 --- /dev/null +++ b/identica-capability/capability-restriction/restriction-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/registry/DefaultRestrictionSignalRegistry.java @@ -0,0 +1,50 @@ +package me.whereareiam.identica.provider.capability.restriction.registry; + +import com.google.inject.Singleton; +import me.whereareiam.identica.provider.capability.restriction.model.RestrictionSignalDescriptor; +import me.whereareiam.identica.provider.capability.restriction.type.RestrictionType; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CopyOnWriteArrayList; + +@Singleton +public class DefaultRestrictionSignalRegistry implements RestrictionSignalRegistry { + private final List descriptors = new CopyOnWriteArrayList<>(); + + @Override + public void register(@NotNull RestrictionSignalDescriptor descriptor) { + descriptors.removeIf(existing -> existing.getRestrictionType().equals(descriptor.getRestrictionType()) + && existing.getSignal().equals(descriptor.getSignal())); + descriptors.add(descriptor); + } + + @Override + public void unregister(@NotNull RestrictionSignalDescriptor descriptor) { + descriptors.removeIf(existing -> existing.getRestrictionType().equals(descriptor.getRestrictionType()) + && existing.getSignal().equals(descriptor.getSignal())); + } + + @Override + public @NotNull Optional find(@NotNull RestrictionType type, @NotNull String signalId) { + return descriptors.stream() + .filter(descriptor -> descriptor.getRestrictionType().equals(type)) + .filter(descriptor -> descriptor.getSignal().matches(signalId)) + .findFirst(); + } + + @Override + public @NotNull List signals(@NotNull RestrictionType type) { + List resolved = new ArrayList<>(); + for (RestrictionSignalDescriptor descriptor : descriptors) { + if (descriptor.getRestrictionType().equals(type)) + resolved.add(descriptor); + } + + resolved.sort(Comparator.comparing(descriptor -> descriptor.getSignal().getId(), String.CASE_INSENSITIVE_ORDER)); + return List.copyOf(resolved); + } +} diff --git a/identica-capability/capability-restriction/restriction-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/registry/DefaultRestrictionTypeRegistry.java b/identica-capability/capability-restriction/restriction-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/registry/DefaultRestrictionTypeRegistry.java new file mode 100644 index 000000000..f24ed06e1 --- /dev/null +++ b/identica-capability/capability-restriction/restriction-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/registry/DefaultRestrictionTypeRegistry.java @@ -0,0 +1,43 @@ +package me.whereareiam.identica.provider.capability.restriction.registry; + +import com.google.inject.Singleton; +import me.whereareiam.identica.provider.capability.restriction.model.RestrictionTypeDescriptor; +import me.whereareiam.identica.provider.capability.restriction.registry.type.RestrictionTypeRegistry; +import me.whereareiam.identica.provider.capability.restriction.type.RestrictionType; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CopyOnWriteArrayList; + +@Singleton +public class DefaultRestrictionTypeRegistry implements RestrictionTypeRegistry { + private final List descriptors = new CopyOnWriteArrayList<>(); + + @Override + public void register(@NotNull RestrictionTypeDescriptor descriptor) { + descriptors.removeIf(existing -> existing.getType().equals(descriptor.getType())); + descriptors.add(descriptor); + } + + @Override + public void unregister(@NotNull RestrictionTypeDescriptor descriptor) { + descriptors.removeIf(existing -> existing.getType().equals(descriptor.getType())); + } + + @Override + public @NotNull Optional find(@NotNull RestrictionType type) { + return descriptors.stream() + .filter(descriptor -> descriptor.getType().equals(type)) + .findFirst(); + } + + @Override + public @NotNull List values() { + List resolved = new ArrayList<>(descriptors); + resolved.sort(Comparator.comparing(descriptor -> descriptor.getType().getId(), String.CASE_INSENSITIVE_ORDER)); + return List.copyOf(resolved); + } +} diff --git a/identica-capability/capability-restriction/restriction-join/join-api/build.gradle.kts b/identica-capability/capability-restriction/restriction-join/join-api/build.gradle.kts new file mode 100644 index 000000000..9cae3470d --- /dev/null +++ b/identica-capability/capability-restriction/restriction-join/join-api/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + id("capability") +} + +dependencies { + api(projects.capabilityRestrictionApi) +} + +toolkitPublish { + artifactId.set("restriction-join-api") +} diff --git a/identica-capability/capability-restriction/restriction-join/join-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/JoinRestrictionCapability.java b/identica-capability/capability-restriction/restriction-join/join-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/JoinRestrictionCapability.java new file mode 100644 index 000000000..dc7396d6b --- /dev/null +++ b/identica-capability/capability-restriction/restriction-join/join-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/JoinRestrictionCapability.java @@ -0,0 +1,11 @@ +package me.whereareiam.identica.provider.capability.restriction.join; + +import me.whereareiam.identica.type.provider.capability.ProviderCapability; +import org.jetbrains.annotations.NotNull; + +/** + * Built-in typed join restriction capability identifier. + */ +public final class JoinRestrictionCapability { + public static final @NotNull ProviderCapability CAPABILITY = ProviderCapability.of("restriction-join"); +} diff --git a/identica-capability/capability-restriction/restriction-join/join-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/JoinRestrictionType.java b/identica-capability/capability-restriction/restriction-join/join-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/JoinRestrictionType.java new file mode 100644 index 000000000..c55f0d573 --- /dev/null +++ b/identica-capability/capability-restriction/restriction-join/join-api/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/JoinRestrictionType.java @@ -0,0 +1,11 @@ +package me.whereareiam.identica.provider.capability.restriction.join; + +import me.whereareiam.identica.provider.capability.restriction.type.RestrictionType; +import org.jetbrains.annotations.NotNull; + +/** + * Built-in join restriction type identifier. + */ +public final class JoinRestrictionType { + public static final @NotNull RestrictionType TYPE = RestrictionType.of("join"); +} diff --git a/identica-capability/capability-restriction/restriction-join/join-common/build.gradle.kts b/identica-capability/capability-restriction/restriction-join/join-common/build.gradle.kts new file mode 100644 index 000000000..b6a8393ec --- /dev/null +++ b/identica-capability/capability-restriction/restriction-join/join-common/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("capability") +} + +dependencies { + api(projects.capabilityRestrictionJoinApi) + compileOnly(projects.identicaEngine) + testImplementation(projects.identicaEngine) +} + +toolkitPublish { + artifactId.set("restriction-join-common") +} diff --git a/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/CommandRegistrar.java b/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/CommandRegistrar.java new file mode 100644 index 000000000..4b5bdb67d --- /dev/null +++ b/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/CommandRegistrar.java @@ -0,0 +1,31 @@ +package me.whereareiam.identica.provider.capability.restriction.join; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.google.inject.name.Named; +import me.whereareiam.identica.command.CommandService; +import me.whereareiam.identica.provider.capability.restriction.join.config.JoinRestrictionCommands; + +import java.util.Set; + +@Singleton +public class CommandRegistrar { + private final CommandService commandService; + private final JoinRestrictionCommands commands; + private final Set commandInstances; + + @Inject + public CommandRegistrar( + CommandService commandService, + @Named("joinRestriction") JoinRestrictionCommands commands, + @Named("joinRestrictionCommandInstances") Set commandInstances + ) { + this.commandService = commandService; + this.commands = commands; + this.commandInstances = commandInstances; + } + + public void registerCommands() { + commandService.registerCommandInstances(commands.getCommands(), commandInstances.toArray()); + } +} diff --git a/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/JoinRestrictionGlobalModule.java b/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/JoinRestrictionGlobalModule.java new file mode 100644 index 000000000..b6bd48ed8 --- /dev/null +++ b/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/JoinRestrictionGlobalModule.java @@ -0,0 +1,68 @@ +package me.whereareiam.identica.provider.capability.restriction.join; + +import com.google.inject.AbstractModule; +import com.google.inject.Provides; +import com.google.inject.Singleton; +import com.google.inject.name.Named; +import lombok.RequiredArgsConstructor; +import me.whereareiam.identica.provider.capability.restriction.join.command.JoinRestrictionCommand; +import me.whereareiam.identica.provider.capability.restriction.join.config.JoinRestrictionCommands; +import me.whereareiam.identica.provider.capability.restriction.join.config.JoinRestrictionMessages; +import me.whereareiam.identica.provider.capability.restriction.join.config.provider.JoinRestrictionCommandsProvider; +import me.whereareiam.identica.provider.capability.restriction.join.config.provider.JoinRestrictionMessagesProvider; +import me.whereareiam.identica.provider.capability.restriction.join.config.provider.JoinRestrictionProvidersProvider; +import me.whereareiam.identica.provider.capability.restriction.join.pipeline.JoinPipelineExtension; +import me.whereareiam.identica.provider.capability.restriction.join.pipeline.prepare.group.policy.phase.ApplyJoinRestrictionPhase; +import me.whereareiam.identica.provider.capability.restriction.join.pipeline.scenario.type.authentication.group.identity.phase.EnforceAuthenticationJoinRestrictionPhase; +import me.whereareiam.identica.provider.capability.restriction.join.pipeline.scenario.type.registration.group.identity.phase.EnforceRegistrationJoinRestrictionPhase; + +import java.nio.file.Path; +import java.util.Set; + +@RequiredArgsConstructor +public class JoinRestrictionGlobalModule extends AbstractModule { + private final Path joinRestrictionCapabilityPath; + + @Override + protected void configure() { + bind(JoinRestrictionProvidersProvider.class).asEagerSingleton(); + bind(JoinRestrictionCommandsProvider.class).asEagerSingleton(); + bind(JoinRestrictionMessagesProvider.class).asEagerSingleton(); + bind(JoinRestrictionMessages.class).toProvider(JoinRestrictionMessagesProvider.class); + bind(JoinRestrictionTypeResolver.class).asEagerSingleton(); + bind(ApplyJoinRestrictionPhase.class).asEagerSingleton(); + bind(EnforceAuthenticationJoinRestrictionPhase.class).asEagerSingleton(); + bind(EnforceRegistrationJoinRestrictionPhase.class).asEagerSingleton(); + bind(JoinPipelineExtension.class).asEagerSingleton(); + bind(JoinRestrictionCommand.class).asEagerSingleton(); + bind(CommandRegistrar.class).asEagerSingleton(); + } + + @Provides + @Singleton + @Named("joinRestrictionCapabilityPath") + Path provideJoinRestrictionCapabilityPath() { + return joinRestrictionCapabilityPath; + } + + @Provides + @Singleton + @Named("joinRestriction") + JoinRestrictionCommands provideJoinRestrictionCommands(JoinRestrictionCommandsProvider provider) { + return provider.get(); + } + + @Provides + @Singleton + @Named("joinRestrictionMessages") + JoinRestrictionMessages provideJoinRestrictionMessages(JoinRestrictionMessagesProvider provider) { + return provider.get(); + } + + @Provides + @Singleton + @Named("joinRestrictionCommandInstances") + Set provideJoinRestrictionCommandInstances(JoinRestrictionCommand command) { + return Set.of(command); + } +} diff --git a/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/JoinRestrictionLocalModule.java b/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/JoinRestrictionLocalModule.java new file mode 100644 index 000000000..411ff2206 --- /dev/null +++ b/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/JoinRestrictionLocalModule.java @@ -0,0 +1,14 @@ +package me.whereareiam.identica.provider.capability.restriction.join; + +import com.google.inject.AbstractModule; +import com.google.inject.multibindings.Multibinder; +import me.whereareiam.identica.provider.capability.contribution.ProviderCapabilityContribution; + +public class JoinRestrictionLocalModule extends AbstractModule { + @Override + protected void configure() { + Multibinder.newSetBinder(binder(), ProviderCapabilityContribution.class) + .addBinding() + .to(LinkedJoinSignalContribution.class); + } +} diff --git a/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/JoinRestrictionTypeResolver.java b/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/JoinRestrictionTypeResolver.java new file mode 100644 index 000000000..c7c8ed23f --- /dev/null +++ b/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/JoinRestrictionTypeResolver.java @@ -0,0 +1,96 @@ +package me.whereareiam.identica.provider.capability.restriction.join; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import lombok.RequiredArgsConstructor; +import me.whereareiam.identica.model.config.provider.Providers; +import me.whereareiam.identica.model.provider.InternalProvider; +import me.whereareiam.identica.provider.ProviderManager; +import me.whereareiam.identica.provider.capability.restriction.RestrictionTypeResolver; +import me.whereareiam.identica.provider.capability.restriction.join.config.provider.JoinRestrictionCapabilities; +import me.whereareiam.identica.provider.capability.restriction.join.config.provider.JoinRestrictionProvidersProvider; +import me.whereareiam.identica.provider.capability.restriction.model.RestrictionTypeState; +import me.whereareiam.identica.provider.capability.restriction.type.RestrictionSignal; +import me.whereareiam.identica.provider.capability.restriction.type.RestrictionType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +@Singleton +@RequiredArgsConstructor(onConstructor_ = @Inject) +public class JoinRestrictionTypeResolver implements RestrictionTypeResolver { + private final ProviderManager providerManager; + private final JoinRestrictionProvidersProvider providersProvider; + + @Override + public @NotNull RestrictionType type() { + return JoinRestrictionType.TYPE; + } + + @Override + public @NotNull Optional resolve(@Nullable String providerId) { + InternalProvider provider = findProvider(providerId); + if (provider == null || provider.getDescriptor() == null) return Optional.empty(); + + String resolvedProviderId = provider.getDescriptor().getId(); + Providers.ProviderEntry entry = findEntry(resolvedProviderId); + JoinRestrictionCapabilities.Restriction.Join join = join(entry); + if (join == null) + return Optional.of(new RestrictionTypeState(resolvedProviderId, false, false, Set.of())); + + Set allow = allow(join); + return Optional.of(new RestrictionTypeState(resolvedProviderId, join.isEnabled(), true, allow)); + } + + @Override + public @NotNull List resolveAll() { + List states = new ArrayList<>(); + for (InternalProvider provider : providerManager.getProviders()) { + if (provider == null || provider.getDescriptor() == null) continue; + if (!provider.getDescriptor().hasCapability(JoinRestrictionCapability.CAPABILITY)) continue; + + resolve(provider.getDescriptor().getId()).ifPresent(states::add); + } + + return List.copyOf(states); + } + + private @Nullable InternalProvider findProvider(@Nullable String providerId) { + if (providerId == null || providerId.isBlank()) return null; + for (InternalProvider provider : providerManager.getProviders()) { + if (provider == null || provider.getDescriptor() == null) continue; + if (provider.getDescriptor().getId().equalsIgnoreCase(providerId)) + return provider; + } + return null; + } + + private @Nullable Providers.ProviderEntry findEntry(@NotNull String providerId) { + for (Providers.ProviderEntry entry : providersProvider.get().getProviders()) { + if (entry == null || entry.getId().isBlank()) continue; + if (entry.getId().equalsIgnoreCase(providerId)) + return entry; + } + + return null; + } + + private @Nullable JoinRestrictionCapabilities.Restriction.Join join(@Nullable Providers.ProviderEntry entry) { + if (entry == null) return null; + if (!(entry.getCapabilities() instanceof JoinRestrictionCapabilities capabilities)) return null; + if (capabilities.getRestriction() == null) return null; + + return capabilities.getRestriction().getJoin(); + } + + private @NotNull Set allow(@NotNull JoinRestrictionCapabilities.Restriction.Join join) { + LinkedHashSet resolved = new LinkedHashSet<>(); + for (String signal : join.getAllow()) { + if (signal == null || signal.isBlank()) continue; + resolved.add(RestrictionSignal.of(signal.trim().toLowerCase(Locale.ROOT))); + } + + return Set.copyOf(resolved); + } +} diff --git a/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/LinkedJoinSignalContribution.java b/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/LinkedJoinSignalContribution.java new file mode 100644 index 000000000..4df2fd3c6 --- /dev/null +++ b/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/LinkedJoinSignalContribution.java @@ -0,0 +1,45 @@ +package me.whereareiam.identica.provider.capability.restriction.join; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import lombok.RequiredArgsConstructor; +import me.whereareiam.identica.database.provider.ProviderLinkPersistenceService; +import me.whereareiam.identica.provider.capability.restriction.contribution.ProviderRestrictionSignalContribution; +import me.whereareiam.identica.provider.capability.restriction.model.RestrictionEvaluationRequest; +import me.whereareiam.identica.provider.capability.restriction.type.RestrictionSignal; +import me.whereareiam.identica.provider.capability.restriction.type.RestrictionType; +import me.whereareiam.identica.type.provider.capability.ProviderCapability; +import org.jetbrains.annotations.NotNull; + +@Singleton +@RequiredArgsConstructor(onConstructor_ = @Inject) +public class LinkedJoinSignalContribution implements ProviderRestrictionSignalContribution { + private static final RestrictionSignal LINKED = RestrictionSignal.of("linked"); + + private final ProviderLinkPersistenceService providerLinkPersistenceService; + + @Override + public @NotNull ProviderCapability capability() { + return JoinRestrictionCapability.CAPABILITY; + } + + @Override + public @NotNull RestrictionType restrictionType() { + return JoinRestrictionType.TYPE; + } + + @Override + public @NotNull RestrictionSignal signal() { + return LINKED; + } + + @Override + public boolean matches(@NotNull RestrictionEvaluationRequest request) { + String providerId = request.getProviderId(); + String providerSubject = request.getProviderSubject(); + if (providerId == null || providerId.isBlank()) return false; + if (providerSubject == null || providerSubject.isBlank()) return false; + + return providerLinkPersistenceService.findBySubject(providerId, providerSubject).isPresent(); + } +} diff --git a/identica-adapter-command/src/main/java/me/whereareiam/identica/adapter/command/executor/admin/ProviderRestrictionCommand.java b/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/command/JoinRestrictionCommand.java similarity index 56% rename from identica-adapter-command/src/main/java/me/whereareiam/identica/adapter/command/executor/admin/ProviderRestrictionCommand.java rename to identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/command/JoinRestrictionCommand.java index afefbbe1b..8cc39cade 100644 --- a/identica-adapter-command/src/main/java/me/whereareiam/identica/adapter/command/executor/admin/ProviderRestrictionCommand.java +++ b/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/command/JoinRestrictionCommand.java @@ -1,19 +1,19 @@ -package me.whereareiam.identica.adapter.command.executor.admin; +package me.whereareiam.identica.provider.capability.restriction.join.command; import com.google.inject.Inject; import com.google.inject.Provider; import lombok.RequiredArgsConstructor; import me.whereareiam.identica.Serializer; -import me.whereareiam.identica.adapter.command.suggestion.ProviderIdSuggestions; import me.whereareiam.identica.annotation.Argument; import me.whereareiam.identica.annotation.Command; import me.whereareiam.identica.annotation.Definition; import me.whereareiam.identica.annotation.Suggestions; -import me.whereareiam.identica.model.config.Messages; -import me.whereareiam.identica.model.provider.restriction.ProviderJoinRestrictionStatus; -import me.whereareiam.identica.provider.restriction.ProviderJoinRestrictionService; import me.whereareiam.identica.provider.ProviderOperations; -import me.whereareiam.identica.type.provider.ProviderJoinRestrictionCondition; +import me.whereareiam.identica.provider.capability.restriction.RestrictionService; +import me.whereareiam.identica.provider.capability.restriction.join.JoinRestrictionType; +import me.whereareiam.identica.provider.capability.restriction.join.config.JoinRestrictionMessages; +import me.whereareiam.identica.provider.capability.restriction.model.RestrictionStatus; +import me.whereareiam.identica.provider.capability.restriction.type.RestrictionSignal; import me.whereareiam.keystone.Actor; import me.whereareiam.keystone.model.SerializerContent; import me.whereareiam.keystone.model.SerializerOptions; @@ -24,25 +24,25 @@ import java.util.stream.Collectors; @RequiredArgsConstructor(onConstructor_ = @Inject) -public class ProviderRestrictionCommand { - private final Provider messagesProvider; - private final ProviderJoinRestrictionService restrictionService; +public class JoinRestrictionCommand { + private final Provider messagesProvider; + private final RestrictionService restrictionService; private final ProviderOperations providerOperations; - @Definition("admin-provider-restriction-enable") - @Command("identica admin provider restriction enable ") + @Definition("admin-provider-restriction-join-enable") + @Command("identica admin provider restriction join enable ") public void enable( @NotNull Actor sender, - @Argument("provider") @Suggestions(ProviderIdSuggestions.KEY) String providerId + @Argument("provider") @Suggestions("providerId") String providerId ) { - Messages.Commands.Admin.ProviderRestriction messages = messages(); - Optional current = restrictionService.status(providerId); + JoinRestrictionMessages.Commands messages = messages(); + Optional current = restrictionService.status(JoinRestrictionType.TYPE, providerId); if (current.isEmpty()) { sendMessage(sender, messages.getProviderNotFound(), Map.of("provider", providerId)); return; } - ProviderJoinRestrictionStatus status = restrictionService.enable(providerId); + RestrictionStatus status = restrictionService.enable(JoinRestrictionType.TYPE, providerId); if (!status.isActive()) { sendMessage(sender, messages.getEnableFailed(), Map.of("provider", status.getProviderId())); return; @@ -51,36 +51,36 @@ public void enable( sendMessage(sender, messages.getEnabled(), placeholders(status)); } - @Definition("admin-provider-restriction-disable") - @Command("identica admin provider restriction disable ") + @Definition("admin-provider-restriction-join-disable") + @Command("identica admin provider restriction join disable ") public void disable( @NotNull Actor sender, - @Argument("provider") @Suggestions(ProviderIdSuggestions.KEY) String providerId + @Argument("provider") @Suggestions("providerId") String providerId ) { - Messages.Commands.Admin.ProviderRestriction messages = messages(); - Optional current = restrictionService.status(providerId); + JoinRestrictionMessages.Commands messages = messages(); + Optional current = restrictionService.status(JoinRestrictionType.TYPE, providerId); if (current.isEmpty()) { sendMessage(sender, messages.getProviderNotFound(), Map.of("provider", providerId)); return; } - ProviderJoinRestrictionStatus status = restrictionService.disable(providerId); + RestrictionStatus status = restrictionService.disable(JoinRestrictionType.TYPE, providerId); sendMessage(sender, messages.getDisabled(), placeholders(status)); } - @Definition("admin-provider-restriction-status") - @Command("identica admin provider restriction status [provider]") + @Definition("admin-provider-restriction-join-status") + @Command("identica admin provider restriction join status [provider]") public void status( @NotNull Actor sender, - @Argument("provider") @Suggestions(ProviderIdSuggestions.KEY) @Nullable String providerId + @Argument("provider") @Suggestions("providerId") @Nullable String providerId ) { if (providerId == null || providerId.isBlank()) { list(sender); return; } - Messages.Commands.Admin.ProviderRestriction messages = messages(); - ProviderJoinRestrictionStatus status = restrictionService.status(providerId).orElse(null); + JoinRestrictionMessages.Commands messages = messages(); + RestrictionStatus status = restrictionService.status(JoinRestrictionType.TYPE, providerId).orElse(null); if (status == null) { sendMessage(sender, messages.getStatus().getNotFound(), Map.of("provider", providerId)); return; @@ -91,20 +91,19 @@ public void status( } private void list(@NotNull Actor sender) { - Messages.Commands.Admin.ProviderRestriction messages = messages(); - Messages.Commands.Admin.ProviderRestriction.Status.Listing listing = messages.getStatus().getList(); - List statuses = restrictionService.statuses(); + JoinRestrictionMessages.Commands.Status.Listing listing = messages().getStatus().getList(); + List statuses = restrictionService.statuses(JoinRestrictionType.TYPE); if (statuses.isEmpty()) { sendMessage(sender, listing.getEmpty(), Map.of()); return; } List entries = new ArrayList<>(); - for (ProviderJoinRestrictionStatus status : statuses) { + for (RestrictionStatus status : statuses) { String template = !status.getAllow().isEmpty() ? listing.getEntries().getPopulated() : listing.getEntries().getEmpty(); - String entry = formatLine(template, placeholders(status), placeholderFormat()); + String entry = formatLine(template, placeholders(status)); if (!entry.isBlank()) entries.add(entry); } @@ -117,11 +116,11 @@ private void list(@NotNull Actor sender) { sender.sendMessage(Serializer.serialize(sender, body)); } - private Messages.Commands.Admin.ProviderRestriction messages() { - return messagesProvider.get().getCommands().getAdmin().getProviderRestriction(); + private JoinRestrictionMessages.Commands messages() { + return messagesProvider.get().getCommands(); } - private Map placeholders(@NotNull ProviderJoinRestrictionStatus status) { + private Map placeholders(@NotNull RestrictionStatus status) { String providerName = providerOperations.displayProviderName(status.getProviderId()); Map placeholders = new HashMap<>(); placeholders.put("provider", status.getProviderId()); @@ -130,19 +129,18 @@ private Map placeholders(@NotNull ProviderJoinRestrictionStatus placeholders.put("status", statusLabel(status.isActive())); placeholders.put("active", String.valueOf(status.isActive())); placeholders.put("allow", describeAllow(status.getAllow())); - return placeholders; } private @NotNull String statusLabel(boolean active) { - Messages.Commands.Admin.ProviderRestriction.Status.Labels status = messages().getStatus().getLabels(); - return active ? status.getEnabled() : status.getDisabled(); + JoinRestrictionMessages.Commands.Status.Labels labels = messages().getStatus().getLabels(); + return active ? labels.getEnabled() : labels.getDisabled(); } - private @NotNull String describeAllow(@NotNull Set allow) { + private @NotNull String describeAllow(@NotNull Set allow) { if (allow.isEmpty()) return "NONE"; return allow.stream() - .map(Enum::name) + .map(RestrictionSignal::getId) .collect(Collectors.joining(", ")); } @@ -152,13 +150,12 @@ private void sendMessage(@NotNull Actor sender, @NotNull String message, @NotNul .message(message) .placeholders(placeholders) .build(); - sender.sendMessage(Serializer.serialize(content)); } private String formatLines(@NotNull List lines, @NotNull Map placeholders) { List resolved = new ArrayList<>(); - String entriesToken = placeholderFormat().format("entries"); + String entriesToken = Serializer.getEngine().getPlaceholderFormat().format("entries"); for (String line : lines) { if (line.contains(entriesToken)) { String entries = placeholders.getOrDefault("entries", ""); @@ -166,26 +163,18 @@ private String formatLines(@NotNull List lines, @NotNull Map placeholders, - @NotNull SerializerOptions.PlaceholderFormat format - ) { + private String formatLine(@NotNull String line, @NotNull Map placeholders) { String resolved = line; - for (Map.Entry entry : placeholders.entrySet()) { + SerializerOptions.PlaceholderFormat format = Serializer.getEngine().getPlaceholderFormat(); + for (Map.Entry entry : placeholders.entrySet()) resolved = resolved.replace(format.format(entry.getKey()), entry.getValue() == null ? "" : entry.getValue()); - } return resolved; } - - private SerializerOptions.PlaceholderFormat placeholderFormat() { - return Serializer.getEngine().getPlaceholderFormat(); - } } diff --git a/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/config/JoinRestrictionCommands.java b/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/config/JoinRestrictionCommands.java new file mode 100644 index 000000000..1791218f2 --- /dev/null +++ b/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/config/JoinRestrictionCommands.java @@ -0,0 +1,9 @@ +package me.whereareiam.identica.provider.capability.restriction.join.config; + +import me.whereareiam.identica.model.config.Commands; + +/** + * Command configuration for the typed join restriction module. + */ +public class JoinRestrictionCommands extends Commands { +} diff --git a/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/config/JoinRestrictionMessages.java b/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/config/JoinRestrictionMessages.java new file mode 100644 index 000000000..26d06d5fc --- /dev/null +++ b/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/config/JoinRestrictionMessages.java @@ -0,0 +1,66 @@ +package me.whereareiam.identica.provider.capability.restriction.join.config; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import me.whereareiam.configura.ConfigDocument; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +/** + * Message configuration for the typed join restriction module. + */ +@Getter +@Setter +@ToString +public class JoinRestrictionMessages extends ConfigDocument { + private @NotNull List denied; + private @NotNull Commands commands; + + @Getter + @Setter + @ToString + public static class Commands { + private @NotNull String enabled; + private @NotNull String disabled; + private @NotNull String enableFailed; + private @NotNull String providerNotFound; + private @NotNull Status status; + + @Getter + @Setter + @ToString + public static class Status { + private @NotNull String notFound; + private @NotNull List body; + private @NotNull Labels labels; + private @NotNull Listing list; + + @Getter + @Setter + @ToString + public static class Labels { + private @NotNull String enabled; + private @NotNull String disabled; + } + + @Getter + @Setter + @ToString + public static class Listing { + private @NotNull List body; + private @NotNull String empty; + private @NotNull Entries entries; + + @Getter + @Setter + @ToString + public static class Entries { + private @NotNull String populated; + private @NotNull String empty; + } + } + } + } +} diff --git a/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/config/defaults/JoinRestrictionCommandsDefaults.java b/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/config/defaults/JoinRestrictionCommandsDefaults.java new file mode 100644 index 000000000..5b4abd63b --- /dev/null +++ b/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/config/defaults/JoinRestrictionCommandsDefaults.java @@ -0,0 +1,57 @@ +package me.whereareiam.identica.provider.capability.restriction.join.config.defaults; + +import com.google.inject.Singleton; +import me.whereareiam.configura.merge.defaults.DefaultsProvider; +import me.whereareiam.identica.model.CommandDefinition; +import me.whereareiam.identica.provider.capability.restriction.join.config.JoinRestrictionCommands; + +import java.util.List; +import java.util.Map; + +@Singleton +public class JoinRestrictionCommandsDefaults implements DefaultsProvider { + @Override + public JoinRestrictionCommands supply(JoinRestrictionCommands commands) { + CommandDefinition enable = definition( + "admin provider restriction join enable", + "Enable configured provider join restriction", + "{command} {alias} " + ); + enable.setArguments(Map.of("provider", "Provider id")); + + CommandDefinition disable = definition( + "admin provider restriction join disable", + "Disable runtime provider join restriction", + "{command} {alias} " + ); + disable.setArguments(Map.of("provider", "Provider id")); + + CommandDefinition status = definition( + "admin provider restriction join status", + "Show provider join restriction status", + "{command} {alias} [provider]" + ); + status.setArguments(Map.of("provider", "Provider id")); + + commands.getCommands().put("admin-provider-restriction-join-enable", enable); + commands.getCommands().put("admin-provider-restriction-join-disable", disable); + commands.getCommands().put("admin-provider-restriction-join-status", status); + return commands; + } + + private CommandDefinition definition(String alias, String description, String usage) { + return CommandDefinition.builder() + .enabled(true) + .aliases(List.of(alias)) + .permission("identica.admin.provider.restriction") + .description(description) + .usage(usage) + .cooldown(CommandDefinition.Cooldown.builder() + .enabled(true) + .duration(2) + .group("global") + .build() + ) + .build(); + } +} diff --git a/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/config/defaults/JoinRestrictionMessagesDefaults.java b/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/config/defaults/JoinRestrictionMessagesDefaults.java new file mode 100644 index 000000000..1458bade4 --- /dev/null +++ b/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/config/defaults/JoinRestrictionMessagesDefaults.java @@ -0,0 +1,68 @@ +package me.whereareiam.identica.provider.capability.restriction.join.config.defaults; + +import com.google.inject.Singleton; +import me.whereareiam.configura.merge.defaults.DefaultsProvider; +import me.whereareiam.identica.provider.capability.restriction.join.config.JoinRestrictionMessages; + +import java.util.List; + +@Singleton +public class JoinRestrictionMessagesDefaults implements DefaultsProvider { + @Override + public JoinRestrictionMessages supply(JoinRestrictionMessages messages) { + messages.setDenied(List.of( + "ɪᴅᴇɴᴛɪᴄᴀ", + "", + "{providerName} is temporarily unavailable.", + "Allowed joins currently require: {allow}.", + "If you think this is a mistake, contact a server administrator.", + "", + "discord.arcadeya.com" + )); + + JoinRestrictionMessages.Commands commands = new JoinRestrictionMessages.Commands(); + commands.setEnabled("{prefix}Enabled provider restriction for {providerName} ({providerId}) with {allow} allowed."); + commands.setDisabled("{prefix}Disabled provider restriction for {providerName} ({providerId})."); + commands.setEnableFailed("{prefix}Could not enable provider restriction for {provider}."); + commands.setProviderNotFound("{prefix}Unknown provider {provider}."); + + JoinRestrictionMessages.Commands.Status status = new JoinRestrictionMessages.Commands.Status(); + status.setNotFound("{prefix}Unknown provider {provider}."); + status.setBody(List.of( + " ", + " Identica", + " ", + " Provider restriction for {providerName} ({providerId})", + " ", + " Runtime active: {active}", + " Allow: {allow}", + " " + )); + + JoinRestrictionMessages.Commands.Status.Labels labels = new JoinRestrictionMessages.Commands.Status.Labels(); + labels.setEnabled("Enabled"); + labels.setDisabled("Disabled"); + status.setLabels(labels); + + JoinRestrictionMessages.Commands.Status.Listing listing = new JoinRestrictionMessages.Commands.Status.Listing(); + listing.setBody(List.of( + " ", + " Identica", + " ", + " Provider restriction status:", + "{entries}", + " " + )); + listing.setEmpty("{prefix}No providers available."); + + JoinRestrictionMessages.Commands.Status.Listing.Entries entries = + new JoinRestrictionMessages.Commands.Status.Listing.Entries(); + entries.setPopulated(" ▪ {providerName}: {status}\n [{allow}]"); + entries.setEmpty(" ▪ {providerName}: {status}"); + listing.setEntries(entries); + status.setList(listing); + commands.setStatus(status); + messages.setCommands(commands); + return messages; + } +} diff --git a/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/config/provider/JoinRestrictionCapabilities.java b/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/config/provider/JoinRestrictionCapabilities.java new file mode 100644 index 000000000..cc34382d3 --- /dev/null +++ b/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/config/provider/JoinRestrictionCapabilities.java @@ -0,0 +1,45 @@ +package me.whereareiam.identica.provider.capability.restriction.join.config.provider; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import me.whereareiam.configura.annotation.merge.Merge; +import me.whereareiam.configura.feature.extension.api.annotation.ExtendableDocument; +import me.whereareiam.configura.merge.strategy.DeclaredObjectDefaults; +import me.whereareiam.identica.model.config.provider.Providers; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** + * Restriction capability provider-specific config extension for the join type. + */ +@Getter +@Setter +@ToString +public class JoinRestrictionCapabilities extends Providers.ProviderEntry.Capabilities { + @Merge(DeclaredObjectDefaults.class) + @ExtendableDocument + private @Nullable Restriction restriction; + + @Getter + @Setter + @ToString + @ExtendableDocument + public static class Restriction { + @Merge(DeclaredObjectDefaults.class) + @ExtendableDocument + private @Nullable Join join; + + @Getter + @Setter + @ToString + @ExtendableDocument + public static class Join { + private boolean enabled; + private @NotNull List allow = new ArrayList<>(); + } + } +} diff --git a/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/config/provider/JoinRestrictionCommandsProvider.java b/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/config/provider/JoinRestrictionCommandsProvider.java new file mode 100644 index 000000000..cd28356e6 --- /dev/null +++ b/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/config/provider/JoinRestrictionCommandsProvider.java @@ -0,0 +1,30 @@ +package me.whereareiam.identica.provider.capability.restriction.join.config.provider; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.google.inject.name.Named; +import me.whereareiam.configura.Config; +import me.whereareiam.configura.Configura; +import me.whereareiam.identica.Registry; +import me.whereareiam.identica.Reloadable; +import me.whereareiam.identica.config.ConfigProvider; +import me.whereareiam.identica.provider.capability.restriction.join.config.JoinRestrictionCommands; +import me.whereareiam.identica.provider.capability.restriction.join.config.defaults.JoinRestrictionCommandsDefaults; + +import java.nio.file.Path; + +@Singleton +public class JoinRestrictionCommandsProvider extends ConfigProvider { + @Inject + public JoinRestrictionCommandsProvider( + @Named("joinRestrictionCapabilityPath") Path joinRestrictionCapabilityPath, + Registry reloadables + ) { + super(joinRestrictionCapabilityPath, "commands", JoinRestrictionCommands.class, reloadables); + } + + @Override + protected Configura configura() { + return versioned(Config.configured().withDefaults(JoinRestrictionCommandsDefaults.class), JoinRestrictionCommands.class); + } +} diff --git a/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/config/provider/JoinRestrictionMessagesProvider.java b/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/config/provider/JoinRestrictionMessagesProvider.java new file mode 100644 index 000000000..dd63e8903 --- /dev/null +++ b/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/config/provider/JoinRestrictionMessagesProvider.java @@ -0,0 +1,30 @@ +package me.whereareiam.identica.provider.capability.restriction.join.config.provider; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.google.inject.name.Named; +import me.whereareiam.configura.Config; +import me.whereareiam.configura.Configura; +import me.whereareiam.identica.Registry; +import me.whereareiam.identica.Reloadable; +import me.whereareiam.identica.config.ConfigProvider; +import me.whereareiam.identica.provider.capability.restriction.join.config.JoinRestrictionMessages; +import me.whereareiam.identica.provider.capability.restriction.join.config.defaults.JoinRestrictionMessagesDefaults; + +import java.nio.file.Path; + +@Singleton +public class JoinRestrictionMessagesProvider extends ConfigProvider { + @Inject + public JoinRestrictionMessagesProvider( + @Named("joinRestrictionCapabilityPath") Path joinRestrictionCapabilityPath, + Registry reloadables + ) { + super(joinRestrictionCapabilityPath, "messages", JoinRestrictionMessages.class, reloadables); + } + + @Override + protected Configura configura() { + return versioned(Config.configured().withDefaults(JoinRestrictionMessagesDefaults.class), JoinRestrictionMessages.class); + } +} diff --git a/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/config/provider/JoinRestrictionProvidersProvider.java b/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/config/provider/JoinRestrictionProvidersProvider.java new file mode 100644 index 000000000..d62de2078 --- /dev/null +++ b/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/config/provider/JoinRestrictionProvidersProvider.java @@ -0,0 +1,45 @@ +package me.whereareiam.identica.provider.capability.restriction.join.config.provider; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.google.inject.name.Named; +import me.whereareiam.configura.Config; +import me.whereareiam.configura.Configura; +import me.whereareiam.configura.document.DocumentTypeContext; +import me.whereareiam.configura.feature.extension.ExtensionFeature; +import me.whereareiam.configura.feature.extension.api.ConfigDocumentRule; +import me.whereareiam.identica.Registry; +import me.whereareiam.identica.Reloadable; +import me.whereareiam.identica.config.ConfigProvider; +import me.whereareiam.identica.model.config.provider.Providers; +import org.jetbrains.annotations.NotNull; + +import java.nio.file.Path; + +@Singleton +public class JoinRestrictionProvidersProvider extends ConfigProvider { + @Inject + public JoinRestrictionProvidersProvider( + @Named("providersPath") Path providersPath, + Registry reloadables + ) { + super(providersPath, "providers", Providers.class, reloadables); + } + + @Override + protected Configura configura() { + return Config.configured().withFeature(ExtensionFeature.rule( + ConfigDocumentRule.when( + Providers.ProviderEntry.Capabilities.class, + JoinRestrictionCapabilities.class, + JoinRestrictionProvidersProvider::isRestrictionCapabilitiesNode + ) + )); + } + + private static boolean isRestrictionCapabilitiesNode(@NotNull DocumentTypeContext context) { + return "capabilities".equals(context.getFieldName()) + && context.getCurrentNode() != null + && context.getCurrentNode().has("restriction"); + } +} diff --git a/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/pipeline/JoinPipelineExtension.java b/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/pipeline/JoinPipelineExtension.java new file mode 100644 index 000000000..7dd306daf --- /dev/null +++ b/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/pipeline/JoinPipelineExtension.java @@ -0,0 +1,41 @@ +package me.whereareiam.identica.provider.capability.restriction.join.pipeline; + +import com.google.inject.Inject; +import lombok.RequiredArgsConstructor; +import me.whereareiam.identica.model.pipeline.phase.PhasePlacement; +import me.whereareiam.identica.pipeline.extension.PipelineExtension; +import me.whereareiam.identica.pipeline.extension.PipelineExtensionBuilder; +import me.whereareiam.identica.provider.capability.restriction.join.pipeline.prepare.group.policy.phase.ApplyJoinRestrictionPhase; +import me.whereareiam.identica.provider.capability.restriction.join.pipeline.scenario.type.authentication.group.identity.phase.EnforceAuthenticationJoinRestrictionPhase; +import me.whereareiam.identica.provider.capability.restriction.join.pipeline.scenario.type.registration.group.identity.phase.EnforceRegistrationJoinRestrictionPhase; +import me.whereareiam.identica.type.pipeline.PipelineType; +import org.jetbrains.annotations.NotNull; + +@RequiredArgsConstructor(onConstructor_ = @Inject) +public class JoinPipelineExtension implements PipelineExtension { + private final ApplyJoinRestrictionPhase applyJoinRestrictionPhase; + private final EnforceAuthenticationJoinRestrictionPhase enforceAuthenticationJoinRestrictionPhase; + private final EnforceRegistrationJoinRestrictionPhase enforceRegistrationJoinRestrictionPhase; + + @Override + public @NotNull String id() { + return "restriction:join:phases"; + } + + @Override + public void apply(@NotNull PipelineExtensionBuilder builder) { + builder.registerPreparePhase("policy", applyJoinRestrictionPhase, PhasePlacement.first()); + builder.registerScenarioPhase( + PipelineType.AUTHENTICATION, + "identity", + enforceAuthenticationJoinRestrictionPhase, + PhasePlacement.after("load-identity-profile") + ); + builder.registerScenarioPhase( + PipelineType.REGISTRATION, + "identity", + enforceRegistrationJoinRestrictionPhase, + PhasePlacement.after("validate-provider") + ); + } +} diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/identity/phase/base/AbstractEnforceProviderJoinRestrictionPhase.java b/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/pipeline/phase/base/AbstractEnforceJoinRestrictionPhase.java similarity index 59% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/identity/phase/base/AbstractEnforceProviderJoinRestrictionPhase.java rename to identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/pipeline/phase/base/AbstractEnforceJoinRestrictionPhase.java index 478f65eb0..ea86e2d17 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/identity/phase/base/AbstractEnforceProviderJoinRestrictionPhase.java +++ b/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/pipeline/phase/base/AbstractEnforceJoinRestrictionPhase.java @@ -1,23 +1,25 @@ -package me.whereareiam.identica.engine.pipeline.scenario.base.identity.phase.base; +package me.whereareiam.identica.provider.capability.restriction.join.pipeline.phase.base; import com.google.inject.Provider; import lombok.RequiredArgsConstructor; import me.whereareiam.identica.Serializer; -import me.whereareiam.identica.engine.pipeline.scenario.base.AbstractGroupState; -import me.whereareiam.identica.engine.pipeline.scenario.base.identity.item.IdentityMetaItem; import me.whereareiam.identica.model.config.Engine; -import me.whereareiam.identica.model.config.Messages; import me.whereareiam.identica.model.pipeline.PipelineResult; import me.whereareiam.identica.model.pipeline.phase.PhaseResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.model.provider.ProviderContext; -import me.whereareiam.identica.model.provider.restriction.ProviderJoinRestrictionDecision; import me.whereareiam.identica.pipeline.PipelinePhase; import me.whereareiam.identica.pipeline.ScenarioContext; +import me.whereareiam.identica.pipeline.state.AbstractGroupState; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.shared.item.IdentityMetaItem; import me.whereareiam.identica.provider.ProviderOperations; -import me.whereareiam.identica.provider.restriction.ProviderJoinRestrictionService; +import me.whereareiam.identica.provider.capability.restriction.RestrictionService; +import me.whereareiam.identica.provider.capability.restriction.join.JoinRestrictionType; +import me.whereareiam.identica.provider.capability.restriction.join.config.JoinRestrictionMessages; +import me.whereareiam.identica.provider.capability.restriction.model.RestrictionDecision; +import me.whereareiam.identica.provider.capability.restriction.model.RestrictionEvaluationRequest; +import me.whereareiam.identica.provider.capability.restriction.type.RestrictionSignal; import me.whereareiam.identica.type.pipeline.PipelineStatus; -import me.whereareiam.identica.type.provider.ProviderJoinRestrictionCondition; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -29,22 +31,19 @@ import java.util.stream.Collectors; @RequiredArgsConstructor -public abstract class AbstractEnforceProviderJoinRestrictionPhase implements PipelinePhase { - private final ProviderJoinRestrictionService restrictionService; +public abstract class AbstractEnforceJoinRestrictionPhase implements PipelinePhase { + private final RestrictionService restrictionService; private final ProviderOperations providerOperations; - private final Provider messagesProvider; + private final Provider messagesProvider; private final Provider engineProvider; @Override public @NotNull String id() { - return "enforce-provider-join-restriction"; + return "enforce-join-restriction"; } @Override - public @NotNull CompletionStage> execute( - @NotNull PipelineState pipelineState, - @NotNull S state - ) { + public @NotNull CompletionStage> execute(@NotNull PipelineState pipelineState, @NotNull S state) { PipelineResult result = state.getResult(); if (result == null || result.getStatus() != PipelineStatus.COMPLETE) return CompletableFuture.completedFuture(PhaseResult.pass(state)); @@ -56,15 +55,16 @@ public abstract class AbstractEnforceProviderJoinRestrictionPhase allow - ) { + private @NotNull String restrictedMessage(@NotNull String providerId, @NotNull Set allow) { String providerName = providerOperations.displayProviderName(providerId); Map placeholders = new HashMap<>(); placeholders.put("providerId", providerId); placeholders.put("providerName", providerName != null ? providerName : providerId); placeholders.put("allow", describeAllow(allow)); - String resolved = String.join("\n", messagesProvider.get().getProviders().getProviderRestriction().getDenied()); + String resolved = String.join("\n", messagesProvider.get().getDenied()); var format = Serializer.getEngine().getPlaceholderFormat(); - for (Map.Entry entry : placeholders.entrySet()) { + for (Map.Entry entry : placeholders.entrySet()) resolved = resolved.replace(format.format(entry.getKey()), entry.getValue() == null ? "" : entry.getValue()); - } return resolved; } - private @NotNull String describeAllow(@NotNull Set allow) { + private @NotNull String describeAllow(@NotNull Set allow) { if (allow.isEmpty()) return "NONE"; return allow.stream() - .map(Enum::name) + .map(RestrictionSignal::getId) .collect(Collectors.joining(", ")); } } diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/policy/phase/ApplyProviderJoinRestrictionPhase.java b/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/pipeline/prepare/group/policy/phase/ApplyJoinRestrictionPhase.java similarity index 59% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/policy/phase/ApplyProviderJoinRestrictionPhase.java rename to identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/pipeline/prepare/group/policy/phase/ApplyJoinRestrictionPhase.java index 822f9f7df..f9a6c8251 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/policy/phase/ApplyProviderJoinRestrictionPhase.java +++ b/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/pipeline/prepare/group/policy/phase/ApplyJoinRestrictionPhase.java @@ -1,42 +1,44 @@ -package me.whereareiam.identica.engine.pipeline.prepare.group.policy.phase; +package me.whereareiam.identica.provider.capability.restriction.join.pipeline.prepare.group.policy.phase; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; import me.whereareiam.identica.Serializer; -import me.whereareiam.identica.engine.pipeline.prepare.group.PrepareGroupState; -import me.whereareiam.identica.model.config.Messages; import me.whereareiam.identica.model.pipeline.phase.PhaseResult; import me.whereareiam.identica.model.pipeline.prepare.PrepareAccountCandidateItem; import me.whereareiam.identica.model.pipeline.prepare.PrepareContextItem; import me.whereareiam.identica.model.pipeline.prepare.decision.PrepareDecisionItem; -import me.whereareiam.identica.model.pipeline.state.PipelineState; -import me.whereareiam.identica.model.provider.restriction.ProviderJoinRestrictionDecision; import me.whereareiam.identica.model.provider.ProviderContext; import me.whereareiam.identica.pipeline.PipelinePhase; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.prepare.PrepareGroupState; import me.whereareiam.identica.provider.ProviderOperations; -import me.whereareiam.identica.provider.restriction.ProviderJoinRestrictionService; -import me.whereareiam.identica.type.provider.ProviderJoinRestrictionCondition; +import me.whereareiam.identica.provider.capability.restriction.RestrictionService; +import me.whereareiam.identica.provider.capability.restriction.join.JoinRestrictionType; +import me.whereareiam.identica.provider.capability.restriction.join.config.JoinRestrictionMessages; +import me.whereareiam.identica.provider.capability.restriction.model.RestrictionDecision; +import me.whereareiam.identica.provider.capability.restriction.model.RestrictionEvaluationRequest; +import me.whereareiam.identica.provider.capability.restriction.type.RestrictionSignal; import org.jetbrains.annotations.NotNull; import java.util.HashMap; import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; +import java.util.stream.Collectors; @Singleton @RequiredArgsConstructor(onConstructor_ = @Inject) -public class ApplyProviderJoinRestrictionPhase implements PipelinePhase { - private final ProviderJoinRestrictionService restrictionService; +public class ApplyJoinRestrictionPhase implements PipelinePhase { + private final RestrictionService restrictionService; private final ProviderOperations providerOperations; - private final Provider messagesProvider; + private final Provider messagesProvider; @Override public @NotNull String id() { - return "apply-provider-join-restriction"; + return "apply-join-restriction"; } @Override @@ -66,31 +68,19 @@ public boolean supports(@NotNull PipelineState pipelineState, @NotNull PrepareGr if (provider == null || provider.getProviderId() == null || provider.getProviderId().isBlank()) return CompletableFuture.completedFuture(PhaseResult.pass(state)); - if (provider.getProviderSubject() == null || provider.getProviderSubject().isBlank()) { - ProviderJoinRestrictionDecision decision = restrictionService.evaluate(provider.getProviderId()); - if (decision.isAllowed()) - return CompletableFuture.completedFuture(PhaseResult.pass(state)); - - pipelineState.putItem(PrepareDecisionItem.deny( - restrictedMessage(provider.getProviderId(), decision.getAllow()), - context, - null, - state.getRequest().getIdentity().getUsername() - ), 0L); - return CompletableFuture.completedFuture(PhaseResult.pass(state)); - } - - PrepareAccountCandidateItem candidate = pipelineState.item(PrepareAccountCandidateItem.class).orElse(null); - ProviderJoinRestrictionDecision decision = restrictionService.evaluate( - provider.getProviderId(), - provider.getProviderSubject(), - provider.getProviderUsername(), - state.getRequest().getIdentity().getIp(), - state.getRequest().getIdentity().getOrigin() - ); + RestrictionDecision decision = restrictionService.evaluate(RestrictionEvaluationRequest.builder() + .type(JoinRestrictionType.TYPE) + .providerId(provider.getProviderId()) + .providerSubject(provider.getProviderSubject()) + .providerUsername(provider.getProviderUsername()) + .connectionUniqueId(state.getRequest().getIdentity().getConnectionUniqueId()) + .ip(state.getRequest().getIdentity().getIp()) + .origin(state.getRequest().getIdentity().getOrigin()) + .build()); if (decision.isAllowed()) return CompletableFuture.completedFuture(PhaseResult.pass(state)); + PrepareAccountCandidateItem candidate = pipelineState.item(PrepareAccountCandidateItem.class).orElse(null); pipelineState.putItem(PrepareDecisionItem.deny( restrictedMessage(provider.getProviderId(), decision.getAllow()), context, @@ -100,37 +90,25 @@ public boolean supports(@NotNull PipelineState pipelineState, @NotNull PrepareGr return CompletableFuture.completedFuture(PhaseResult.pass(state)); } - private @NotNull String restrictedMessage( - @NotNull String providerId, - @NotNull Set allow - ) { - return render(messagesProvider.get().getProviders().getProviderRestriction().getDenied(), providerId, allow); - } - - private @NotNull String render( - @NotNull java.util.List lines, - @NotNull String providerId, - @NotNull Set allow - ) { + private @NotNull String restrictedMessage(@NotNull String providerId, @NotNull Set allow) { String providerName = providerOperations.displayProviderName(providerId); Map placeholders = new HashMap<>(); placeholders.put("providerId", providerId); placeholders.put("providerName", providerName != null ? providerName : providerId); placeholders.put("allow", describeAllow(allow)); - String resolved = String.join("\n", lines); + String resolved = String.join("\n", messagesProvider.get().getDenied()); var format = Serializer.getEngine().getPlaceholderFormat(); - for (Map.Entry entry : placeholders.entrySet()) { + for (Map.Entry entry : placeholders.entrySet()) resolved = resolved.replace(format.format(entry.getKey()), entry.getValue() == null ? "" : entry.getValue()); - } - + return resolved; } - private @NotNull String describeAllow(@NotNull Set allow) { + private @NotNull String describeAllow(@NotNull Set allow) { if (allow.isEmpty()) return "NONE"; return allow.stream() - .map(Enum::name) + .map(RestrictionSignal::getId) .collect(Collectors.joining(", ")); } } diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/identity/phase/EnforceProviderJoinRestrictionPhase.java b/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/pipeline/scenario/type/authentication/group/identity/phase/EnforceAuthenticationJoinRestrictionPhase.java similarity index 63% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/identity/phase/EnforceProviderJoinRestrictionPhase.java rename to identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/pipeline/scenario/type/authentication/group/identity/phase/EnforceAuthenticationJoinRestrictionPhase.java index decafc652..c73b5ff4b 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/identity/phase/EnforceProviderJoinRestrictionPhase.java +++ b/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/pipeline/scenario/type/authentication/group/identity/phase/EnforceAuthenticationJoinRestrictionPhase.java @@ -1,28 +1,27 @@ -package me.whereareiam.identica.engine.pipeline.scenario.authentication.group.identity.phase; +package me.whereareiam.identica.provider.capability.restriction.join.pipeline.scenario.type.authentication.group.identity.phase; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; -import me.whereareiam.identica.engine.pipeline.scenario.authentication.group.identity.IdentityState; -import me.whereareiam.identica.engine.pipeline.scenario.base.identity.phase.base.AbstractEnforceProviderJoinRestrictionPhase; import me.whereareiam.identica.model.auth.AuthContext; import me.whereareiam.identica.model.config.Engine; -import me.whereareiam.identica.model.config.Messages; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.model.provider.ProviderContext; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.type.authentication.IdentityState; import me.whereareiam.identica.provider.ProviderOperations; -import me.whereareiam.identica.provider.restriction.ProviderJoinRestrictionService; +import me.whereareiam.identica.provider.capability.restriction.RestrictionService; +import me.whereareiam.identica.provider.capability.restriction.join.config.JoinRestrictionMessages; +import me.whereareiam.identica.provider.capability.restriction.join.pipeline.phase.base.AbstractEnforceJoinRestrictionPhase; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @Singleton -public class EnforceProviderJoinRestrictionPhase - extends AbstractEnforceProviderJoinRestrictionPhase { +public class EnforceAuthenticationJoinRestrictionPhase extends AbstractEnforceJoinRestrictionPhase { @Inject - public EnforceProviderJoinRestrictionPhase( - ProviderJoinRestrictionService restrictionService, + public EnforceAuthenticationJoinRestrictionPhase( + RestrictionService restrictionService, ProviderOperations providerOperations, - Provider messagesProvider, + Provider messagesProvider, Provider engineProvider ) { super(restrictionService, providerOperations, messagesProvider, engineProvider); diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/identity/phase/EnforceProviderJoinRestrictionPhase.java b/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/pipeline/scenario/type/registration/group/identity/phase/EnforceRegistrationJoinRestrictionPhase.java similarity index 58% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/identity/phase/EnforceProviderJoinRestrictionPhase.java rename to identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/pipeline/scenario/type/registration/group/identity/phase/EnforceRegistrationJoinRestrictionPhase.java index 24a4cf4e0..4f88d8150 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/identity/phase/EnforceProviderJoinRestrictionPhase.java +++ b/identica-capability/capability-restriction/restriction-join/join-common/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/pipeline/scenario/type/registration/group/identity/phase/EnforceRegistrationJoinRestrictionPhase.java @@ -1,28 +1,27 @@ -package me.whereareiam.identica.engine.pipeline.scenario.registration.group.identity.phase; +package me.whereareiam.identica.provider.capability.restriction.join.pipeline.scenario.type.registration.group.identity.phase; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; -import me.whereareiam.identica.engine.pipeline.scenario.registration.group.identity.IdentityState; -import me.whereareiam.identica.engine.pipeline.scenario.base.identity.phase.base.AbstractEnforceProviderJoinRestrictionPhase; import me.whereareiam.identica.model.config.Engine; -import me.whereareiam.identica.model.config.Messages; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.model.provider.ProviderContext; import me.whereareiam.identica.model.registration.RegistrationContext; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.type.registration.IdentityState; import me.whereareiam.identica.provider.ProviderOperations; -import me.whereareiam.identica.provider.restriction.ProviderJoinRestrictionService; +import me.whereareiam.identica.provider.capability.restriction.RestrictionService; +import me.whereareiam.identica.provider.capability.restriction.join.config.JoinRestrictionMessages; +import me.whereareiam.identica.provider.capability.restriction.join.pipeline.phase.base.AbstractEnforceJoinRestrictionPhase; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @Singleton -public class EnforceProviderJoinRestrictionPhase - extends AbstractEnforceProviderJoinRestrictionPhase { +public class EnforceRegistrationJoinRestrictionPhase extends AbstractEnforceJoinRestrictionPhase { @Inject - public EnforceProviderJoinRestrictionPhase( - ProviderJoinRestrictionService restrictionService, + public EnforceRegistrationJoinRestrictionPhase( + RestrictionService restrictionService, ProviderOperations providerOperations, - Provider messagesProvider, + Provider messagesProvider, Provider engineProvider ) { super(restrictionService, providerOperations, messagesProvider, engineProvider); @@ -53,7 +52,7 @@ protected boolean allowResumeBypass(@NotNull Engine settings, @NotNull PipelineS if (!settings.getScenarios().getRegistration().isAllowProviderRestrictionResumeBypass()) return false; - var meta = identityMeta(pipelineState); - return meta != null && meta.isResumed(); + var resumeState = identityMeta(pipelineState); + return resumeState != null && resumeState.isResumed(); } } diff --git a/identica-capability/capability-restriction/restriction-join/join/build.gradle.kts b/identica-capability/capability-restriction/restriction-join/join/build.gradle.kts new file mode 100644 index 000000000..4164b704c --- /dev/null +++ b/identica-capability/capability-restriction/restriction-join/join/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + id("capability-artifact") +} + +dependencies { + api(projects.capabilityRestrictionJoinApi) + implementation(projects.capabilityRestrictionJoinCommon) +} + +toolkitPublish { + artifactId.set("restriction-join") +} diff --git a/identica-capability/capability-restriction/restriction-join/join/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/bootstrap/JoinRestrictionCapabilityBootstrap.java b/identica-capability/capability-restriction/restriction-join/join/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/bootstrap/JoinRestrictionCapabilityBootstrap.java new file mode 100644 index 000000000..f98988104 --- /dev/null +++ b/identica-capability/capability-restriction/restriction-join/join/src/main/java/me/whereareiam/identica/provider/capability/restriction/join/bootstrap/JoinRestrictionCapabilityBootstrap.java @@ -0,0 +1,77 @@ +package me.whereareiam.identica.provider.capability.restriction.join.bootstrap; + +import com.google.inject.Injector; +import com.google.inject.Module; +import me.whereareiam.identica.model.provider.capability.ProviderCapabilityDeclaration; +import me.whereareiam.identica.pipeline.extension.PipelineExtensionRegistry; +import me.whereareiam.identica.provider.capability.bootstrap.ProviderCapabilityBootstrap; +import me.whereareiam.identica.provider.capability.bootstrap.ProviderCapabilityGlobalInstallContext; +import me.whereareiam.identica.provider.capability.bootstrap.ProviderCapabilityInitializationContext; +import me.whereareiam.identica.provider.capability.bootstrap.ProviderCapabilityLocalInstallContext; +import me.whereareiam.identica.provider.capability.restriction.join.*; +import me.whereareiam.identica.provider.capability.restriction.join.pipeline.JoinPipelineExtension; +import me.whereareiam.identica.provider.capability.restriction.model.RestrictionSignalDescriptor; +import me.whereareiam.identica.provider.capability.restriction.model.RestrictionTypeDescriptor; +import me.whereareiam.identica.provider.capability.restriction.registry.RestrictionSignalRegistry; +import me.whereareiam.identica.provider.capability.restriction.registry.type.RestrictionTypeRegistry; +import me.whereareiam.identica.provider.capability.restriction.registry.type.RestrictionTypeResolverRegistry; +import me.whereareiam.identica.provider.capability.restriction.type.RestrictionSignal; +import me.whereareiam.identica.type.provider.capability.ProviderCapabilityScope; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Set; + +/** + * Bootstrap for the typed join restriction capability. + */ +public final class JoinRestrictionCapabilityBootstrap implements ProviderCapabilityBootstrap { + public static final @NotNull JoinRestrictionCapabilityBootstrap INSTANCE = new JoinRestrictionCapabilityBootstrap(); + + @Override + public @NotNull ProviderCapabilityDeclaration declaration() { + return ProviderCapabilityDeclaration.builder() + .capability(JoinRestrictionCapability.CAPABILITY) + .scopes(Set.of(ProviderCapabilityScope.GLOBAL, ProviderCapabilityScope.LOCAL)) + .build(); + } + + @Override + public void initialize(@NotNull ProviderCapabilityInitializationContext context) { + Injector globalInjector = context.getGlobalInjector(); + if (globalInjector == null) return; + + globalInjector.getInstance(RestrictionTypeRegistry.class).register(RestrictionTypeDescriptor.builder() + .type(JoinRestrictionType.TYPE) + .displayName("Join") + .description("Runtime restriction evaluated during connection preparation and scenario entry.") + .build()); + globalInjector.getInstance(RestrictionSignalRegistry.class).register(RestrictionSignalDescriptor.builder() + .restrictionType(JoinRestrictionType.TYPE) + .signal(RestrictionSignal.of("linked")) + .displayName("Linked") + .description("Allows already-linked provider subjects through join restriction.") + .build()); + globalInjector.getInstance(RestrictionTypeResolverRegistry.class) + .register(globalInjector.getInstance(JoinRestrictionTypeResolver.class)); + globalInjector.getInstance(CommandRegistrar.class).registerCommands(); + + context.getRootInjector() + .getInstance(PipelineExtensionRegistry.class) + .register(globalInjector.getInstance(JoinPipelineExtension.class)); + } + + @Override + public @NotNull List globalModules(@NotNull ProviderCapabilityGlobalInstallContext context) { + return List.of(new JoinRestrictionGlobalModule( + context.getCapabilitiesPath() + .resolve("restriction") + .resolve("join") + )); + } + + @Override + public @NotNull List localModules(@NotNull ProviderCapabilityLocalInstallContext context) { + return List.of(new JoinRestrictionLocalModule()); + } +} diff --git a/identica-capability/capability-restriction/restriction/build.gradle.kts b/identica-capability/capability-restriction/restriction/build.gradle.kts new file mode 100644 index 000000000..e33f8983d --- /dev/null +++ b/identica-capability/capability-restriction/restriction/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + id("capability-artifact") +} + +dependencies { + api(projects.capabilityRestrictionApi) + implementation(projects.capabilityRestrictionCommon) +} + +toolkitPublish { + artifactId.set("restriction") +} diff --git a/identica-capability/capability-restriction/restriction/src/main/java/me/whereareiam/identica/provider/capability/restriction/bootstrap/RestrictionCapabilityBootstrap.java b/identica-capability/capability-restriction/restriction/src/main/java/me/whereareiam/identica/provider/capability/restriction/bootstrap/RestrictionCapabilityBootstrap.java new file mode 100644 index 000000000..83ff80362 --- /dev/null +++ b/identica-capability/capability-restriction/restriction/src/main/java/me/whereareiam/identica/provider/capability/restriction/bootstrap/RestrictionCapabilityBootstrap.java @@ -0,0 +1,46 @@ +package me.whereareiam.identica.provider.capability.restriction.bootstrap; + +import com.google.inject.Injector; +import com.google.inject.Module; +import me.whereareiam.identica.model.provider.capability.ProviderCapabilityDeclaration; +import me.whereareiam.identica.provider.capability.bootstrap.ProviderCapabilityBootstrap; +import me.whereareiam.identica.provider.capability.bootstrap.ProviderCapabilityGlobalInstallContext; +import me.whereareiam.identica.provider.capability.bootstrap.ProviderCapabilityInitializationContext; +import me.whereareiam.identica.provider.capability.restriction.RestrictionCapability; +import me.whereareiam.identica.provider.capability.restriction.RestrictionGlobalModule; +import me.whereareiam.identica.provider.capability.restriction.config.RestrictionSettings; +import me.whereareiam.identica.type.provider.capability.ProviderCapabilityScope; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Set; + +/** + * Bootstrap for the generic restriction capability runtime. + */ +public final class RestrictionCapabilityBootstrap implements ProviderCapabilityBootstrap { + public static final @NotNull RestrictionCapabilityBootstrap INSTANCE = new RestrictionCapabilityBootstrap(); + + @Override + public @NotNull ProviderCapabilityDeclaration declaration() { + return ProviderCapabilityDeclaration.builder() + .capability(RestrictionCapability.CAPABILITY) + .scopes(Set.of(ProviderCapabilityScope.GLOBAL)) + .build(); + } + + @Override + public void initialize(@NotNull ProviderCapabilityInitializationContext context) { + Injector globalInjector = context.getGlobalInjector(); + if (globalInjector == null) return; + + globalInjector.getInstance(RestrictionSettings.class); + } + + @Override + public @NotNull List globalModules(@NotNull ProviderCapabilityGlobalInstallContext context) { + return List.of(new RestrictionGlobalModule( + context.getCapabilitiesPath().resolve(RestrictionCapability.CAPABILITY.getId()) + )); + } +} diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/CommonConfiguration.java b/identica-common/src/main/java/me/whereareiam/identica/common/CommonConfiguration.java index fd209dbc4..bf1c92506 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/CommonConfiguration.java +++ b/identica-common/src/main/java/me/whereareiam/identica/common/CommonConfiguration.java @@ -11,8 +11,8 @@ import me.whereareiam.identica.common.config.ConfigBindings; import me.whereareiam.identica.common.config.IdenticaModule; import me.whereareiam.identica.common.config.resolver.FileSystemConfigurationTypeResolver; -import me.whereareiam.identica.common.connection.ConnectionStateConfiguration; import me.whereareiam.identica.common.conflict.ConflictConfiguration; +import me.whereareiam.identica.common.connection.ConnectionStateConfiguration; import me.whereareiam.identica.common.event.EventController; import me.whereareiam.identica.common.identity.IdentityConfiguration; import me.whereareiam.identica.common.identity.session.SessionConfiguration; @@ -25,7 +25,6 @@ import me.whereareiam.identica.common.replication.ReplicationConfiguration; import me.whereareiam.identica.common.routing.RoutingConfiguration; import me.whereareiam.identica.common.sentinel.SentinelConfiguration; -import me.whereareiam.identica.common.verification.VerificationConfiguration; import me.whereareiam.identica.config.ConfigurationTypeResolver; import me.whereareiam.identica.event.EventManager; import me.whereareiam.identica.logging.BannerContributor; @@ -35,6 +34,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; + @RequiredArgsConstructor public class CommonConfiguration extends AbstractModule { private final Path dataPath; @@ -57,7 +57,6 @@ protected void configure() { install(new ReplicationConfiguration()); install(new IdentityConfiguration()); install(new SessionConfiguration()); - install(new VerificationConfiguration()); install(new RoutingConfiguration()); install(new ConflictConfiguration()); install(new ProviderConfiguration()); @@ -109,6 +108,20 @@ Path provideProvidersPath(@Named("dataPath") Path dataPath) { return ensureDirectory(dataPath.resolve("providers"), "providers"); } + @Provides + @Singleton + @Named("capabilitiesPath") + Path provideCapabilitiesPath(@Named("providersPath") Path providersPath) { + return ensureDirectory(providersPath.resolve("capabilities"), "capabilities"); + } + + @Provides + @Singleton + @Named("featuresPath") + Path provideFeaturesPath(@Named("dataPath") Path dataPath) { + return ensureDirectory(dataPath.resolve("features"), "features"); + } + private Path ensureDirectory(Path path, String label) { try { Files.createDirectories(path); diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/adapter/ConnectionDecisionDeliveryCoordinator.java b/identica-common/src/main/java/me/whereareiam/identica/common/adapter/ConnectionDecisionDeliveryCoordinator.java index 62791b406..114e58b9c 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/adapter/ConnectionDecisionDeliveryCoordinator.java +++ b/identica-common/src/main/java/me/whereareiam/identica/common/adapter/ConnectionDecisionDeliveryCoordinator.java @@ -9,8 +9,8 @@ import me.whereareiam.identica.model.delivery.DeliveryRequest; import me.whereareiam.identica.model.delivery.DeliveryTarget; import me.whereareiam.identica.model.pipeline.journey.JourneyStateItem; -import me.whereareiam.identica.model.pipeline.state.PipelineState; -import me.whereareiam.identica.model.pipeline.state.PipelineStateReference; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.PipelineStateReference; import me.whereareiam.identica.pipeline.state.PipelineStateStore; import me.whereareiam.identica.service.DeliveryService; import me.whereareiam.identica.type.messaging.DeliveryCheckpoint; diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/config/ConfigBindings.java b/identica-common/src/main/java/me/whereareiam/identica/common/config/ConfigBindings.java index caf8e6ff4..99e5ab254 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/config/ConfigBindings.java +++ b/identica-common/src/main/java/me/whereareiam/identica/common/config/ConfigBindings.java @@ -2,13 +2,7 @@ import com.google.inject.AbstractModule; import me.whereareiam.identica.common.config.provider.*; -import me.whereareiam.identica.model.config.Commands; -import me.whereareiam.identica.model.config.Engine; -import me.whereareiam.identica.model.config.Messages; -import me.whereareiam.identica.model.config.Replication; -import me.whereareiam.identica.model.config.Routing; -import me.whereareiam.identica.model.config.Settings; -import me.whereareiam.identica.model.config.Verification; +import me.whereareiam.identica.model.config.*; import me.whereareiam.identica.model.config.persistence.Persistence; import me.whereareiam.identica.model.config.provider.Conflicts; import me.whereareiam.identica.model.config.provider.Providers; @@ -23,7 +17,6 @@ protected void configure() { bind(Commands.class).toProvider(CommandsProvider.class); bind(Conflicts.class).toProvider(ConflictsProvider.class); bind(Providers.class).toProvider(ProvidersProvider.class); - bind(Verification.class).toProvider(VerificationProvider.class); bind(Persistence.class).toProvider(PersistenceProvider.class); bind(Replication.class).toProvider(ReplicationProvider.class); } diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/config/IdenticaModule.java b/identica-common/src/main/java/me/whereareiam/identica/common/config/IdenticaModule.java index a6fcb82ae..325e54dba 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/config/IdenticaModule.java +++ b/identica-common/src/main/java/me/whereareiam/identica/common/config/IdenticaModule.java @@ -2,17 +2,12 @@ import com.fasterxml.jackson.databind.module.SimpleModule; import me.whereareiam.identica.common.config.deserializer.DateTimePatternDeserializer; -import me.whereareiam.identica.common.config.deserializer.ProviderCapabilityDeserializer; -import me.whereareiam.identica.model.config.type.DateTimePattern; import me.whereareiam.identica.common.config.serializer.DateTimePatternSerializer; -import me.whereareiam.identica.common.config.serializer.ProviderCapabilitySerializer; -import me.whereareiam.identica.type.provider.ProviderCapability; +import me.whereareiam.identica.model.config.type.DateTimePattern; public class IdenticaModule extends SimpleModule { public IdenticaModule() { addSerializer(DateTimePattern.class, new DateTimePatternSerializer()); addDeserializer(DateTimePattern.class, new DateTimePatternDeserializer()); - addSerializer(ProviderCapability.class, new ProviderCapabilitySerializer()); - addDeserializer(ProviderCapability.class, new ProviderCapabilityDeserializer()); } } diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/config/defaults/CommandsDefaults.java b/identica-common/src/main/java/me/whereareiam/identica/common/config/defaults/CommandsDefaults.java index b15639341..83566a21e 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/config/defaults/CommandsDefaults.java +++ b/identica-common/src/main/java/me/whereareiam/identica/common/config/defaults/CommandsDefaults.java @@ -5,7 +5,6 @@ import me.whereareiam.identica.common.config.defaults.commands.AdminCommandDefinitions; import me.whereareiam.identica.common.config.defaults.commands.CoreCommandDefinitions; import me.whereareiam.identica.common.config.defaults.commands.MigrationCommandDefinitions; -import me.whereareiam.identica.common.config.defaults.commands.VerificationCommandDefinitions; import me.whereareiam.identica.common.config.defaults.commands.base.CommandDefinitions; import me.whereareiam.identica.model.config.Commands; @@ -17,8 +16,7 @@ public class CommandsDefaults implements DefaultsProvider { private final List definitions = List.of( new CoreCommandDefinitions(), new AdminCommandDefinitions(), - new MigrationCommandDefinitions(), - new VerificationCommandDefinitions() + new MigrationCommandDefinitions() ); @Override diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/config/defaults/ReplicationDefaults.java b/identica-common/src/main/java/me/whereareiam/identica/common/config/defaults/ReplicationDefaults.java index 4b76298a6..06e88b0c3 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/config/defaults/ReplicationDefaults.java +++ b/identica-common/src/main/java/me/whereareiam/identica/common/config/defaults/ReplicationDefaults.java @@ -28,7 +28,6 @@ public Replication supply(Replication replication) { Replication.Cache cache = new Replication.Cache(); cache.setReservations("identica:reservation"); cache.setInstructions("identica:handshake-instructions"); - cache.setProviderJoinRestrictions("identica:provider-join-restrictions"); cache.setAttempts("identica:provider-attempts"); cache.setSentinels("identica:sentinels"); Replication.Delivery delivery = new Replication.Delivery(); @@ -41,9 +40,6 @@ public Replication supply(Replication replication) { sessions.setUser("identica:sessions:user"); sessions.setSession("identica:sessions:session"); sessions.setSubject("identica:sessions:subject"); - Replication.Sessions.Recognition recognition = new Replication.Sessions.Recognition(); - recognition.setSnapshot("identica:session-recognition:snapshot"); - sessions.setRecognition(recognition); cache.setSessions(sessions); cache.setPipelineState("identica:pipeline-state"); diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/config/defaults/SettingsDefaults.java b/identica-common/src/main/java/me/whereareiam/identica/common/config/defaults/SettingsDefaults.java index bbf877a50..1640cf238 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/config/defaults/SettingsDefaults.java +++ b/identica-common/src/main/java/me/whereareiam/identica/common/config/defaults/SettingsDefaults.java @@ -9,11 +9,9 @@ import me.whereareiam.identica.type.identity.UniqueIdMode; import me.whereareiam.identica.type.platform.PlatformType; import me.whereareiam.identica.type.session.SessionConcurrencyPolicy; -import me.whereareiam.identica.type.session.recognition.RecognitionSignal; import java.time.Duration; import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; @Singleton @@ -42,31 +40,6 @@ private Settings.Sessions defaultSessions() { Settings.Sessions sessions = new Settings.Sessions(); sessions.setConcurrencyPolicy(SessionConcurrencyPolicy.REPLACE_EXISTING); sessions.setActiveTtl(Duration.ofHours(12)); - - Settings.Sessions.Recognition recognition = new Settings.Sessions.Recognition(); - recognition.setEnabled(false); - recognition.setValidity(Duration.ofHours(12)); - recognition.setDefaultSignals(List.of( - RecognitionSignal.USERNAME, - RecognitionSignal.IP, - RecognitionSignal.VIRTUAL_HOST - )); - - Settings.Sessions.Recognition.Eligibility.UntrustedIps untrustedIps = - new Settings.Sessions.Recognition.Eligibility.UntrustedIps(); - untrustedIps.setEnabled(true); - untrustedIps.setEntries(List.of( - "127.0.0.1", - "::1", - "10.0.0.0/8", - "172.16.0.0/12", - "192.168.0.0/16" - )); - - Settings.Sessions.Recognition.Eligibility eligibility = new Settings.Sessions.Recognition.Eligibility(); - eligibility.setUntrustedIps(untrustedIps); - recognition.setEligibility(eligibility); - sessions.setRecognition(recognition); return sessions; } diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/config/defaults/commands/AdminCommandDefinitions.java b/identica-common/src/main/java/me/whereareiam/identica/common/config/defaults/commands/AdminCommandDefinitions.java index c4eeda82b..a69f832b9 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/config/defaults/commands/AdminCommandDefinitions.java +++ b/identica-common/src/main/java/me/whereareiam/identica/common/config/defaults/commands/AdminCommandDefinitions.java @@ -29,44 +29,11 @@ public void register(Registry registry) { registerClear(registry); registerDelete(registry); - registerProviderRestriction(registry); registerReservation(registry); registerSessions(registry); registerVerification(registry); } - private void registerProviderRestriction(Registry registry) { - registry.register("admin-provider-restriction-enable", CommandDefinition.builder() - .enabled(true) - .aliases(List.of("admin provider restriction enable")) - .permission("identica.admin.provider.restriction") - .description("Enable configured provider join restriction") - .usage("{command} {alias} ") - .arguments(Map.of("provider", "Provider id")) - .cooldown(globalCooldown()) - .build()); - - registry.register("admin-provider-restriction-disable", CommandDefinition.builder() - .enabled(true) - .aliases(List.of("admin provider restriction disable")) - .permission("identica.admin.provider.restriction") - .description("Disable runtime provider join restriction") - .usage("{command} {alias} ") - .arguments(Map.of("provider", "Provider id")) - .cooldown(globalCooldown()) - .build()); - - registry.register("admin-provider-restriction-status", CommandDefinition.builder() - .enabled(true) - .aliases(List.of("admin provider restriction status")) - .permission("identica.admin.provider.restriction") - .description("Show provider join restriction status") - .usage("{command} {alias} [provider]") - .arguments(Map.of("provider", "Provider id")) - .cooldown(globalCooldown()) - .build()); - } - private void registerClear(Registry registry) { registry.register("admin-clear", CommandDefinition.builder() .enabled(true) diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/config/defaults/messages/MessagesCommandDefaults.java b/identica-common/src/main/java/me/whereareiam/identica/common/config/defaults/messages/MessagesCommandDefaults.java index fc8ea33d6..444dd6095 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/config/defaults/messages/MessagesCommandDefaults.java +++ b/identica-common/src/main/java/me/whereareiam/identica/common/config/defaults/messages/MessagesCommandDefaults.java @@ -20,7 +20,6 @@ public Messages.Commands supply(Messages.Commands commands) { applyMigration(commands); applyReload(commands); applyAdmin(commands); - applyVerification(commands); return commands; } @@ -113,6 +112,7 @@ private void applyMigration(Messages.Commands commands) { migrationStart.setPendingExists("{prefix}Migration already pending for {target}."); migrationStart.setStarted("{prefix}Migration started for {target} to {provider}."); migrationStart.setProviderUnsupported("{prefix}{provider} does not support migration."); + migrationStart.setProviderUnavailable("{prefix}{provider} is currently unavailable for migration."); migration.setStart(migrationStart); Messages.Commands.Migration.Cancel migrationCancel = new Messages.Commands.Migration.Cancel(); @@ -331,175 +331,9 @@ private void applyAdmin(Messages.Commands commands) { reservation.setNotFound("{prefix}No reservation found for {key}."); reservation.setInvalidKey("{prefix}Invalid reservation key {key}."); - Messages.Commands.Admin.ProviderRestriction providerRestriction = new Messages.Commands.Admin.ProviderRestriction(); - providerRestriction.setEnabled("{prefix}Enabled provider restriction for {providerName} ({providerId}) with {allow} allowed."); - providerRestriction.setDisabled("{prefix}Disabled provider restriction for {providerName} ({providerId})."); - providerRestriction.setEnableFailed("{prefix}Could not enable provider restriction for {provider}."); - providerRestriction.setProviderNotFound("{prefix}Unknown provider {provider}."); - Messages.Commands.Admin.ProviderRestriction.Status providerStatus = - new Messages.Commands.Admin.ProviderRestriction.Status(); - providerStatus.setBody(List.of( - " ", - " Identica", - " ", - " Provider restriction for {providerName} ({providerId})", - " ", - " Runtime active: {active}", - " Allow: {allow}", - " " - )); - providerStatus.setNotFound("{prefix}Unknown provider {provider}."); - Messages.Commands.Admin.ProviderRestriction.Status.Labels providerStatusLabels = - new Messages.Commands.Admin.ProviderRestriction.Status.Labels(); - providerStatusLabels.setEnabled("Enabled"); - providerStatusLabels.setDisabled("Disabled"); - providerStatus.setLabels(providerStatusLabels); - Messages.Commands.Admin.ProviderRestriction.Status.Listing providerListing = - new Messages.Commands.Admin.ProviderRestriction.Status.Listing(); - providerListing.setBody(List.of( - " ", - " Identica", - " ", - " Provider restriction status:", - "{entries}", - " " - )); - Messages.Commands.Admin.ProviderRestriction.Status.Listing.Entries providerEntries = - new Messages.Commands.Admin.ProviderRestriction.Status.Listing.Entries(); - providerEntries.setPopulated(" ▪ {providerName}: {status}\n [{allow}]"); - providerEntries.setEmpty(" ▪ {providerName}: {status}"); - providerListing.setEntries(providerEntries); - providerListing.setEmpty("{prefix}No providers available."); - providerStatus.setList(providerListing); - providerRestriction.setStatus(providerStatus); - admin.setDelete(delete); - admin.setProviderRestriction(providerRestriction); admin.setReservation(reservation); - admin.setVerification(adminVerification()); commands.setAdmin(admin); } - private Messages.Commands.Admin.Verification adminVerification() { - Messages.Commands.Admin.Verification verification = new Messages.Commands.Admin.Verification(); - Messages.Commands.Admin.Verification.Reset reset = new Messages.Commands.Admin.Verification.Reset(); - reset.setTargetNotFound("{prefix}No account found for {target}."); - reset.setCompleted("{prefix}Verification state reset for {target}."); - verification.setReset(reset); - return verification; - } - - private void applyVerification(Messages.Commands commands) { - Messages.Commands.Verification verification = new Messages.Commands.Verification(); - verification.setPlayerOnly("{prefix}This command can only be used by a player."); - verification.setNotAllowed("{prefix}This verification action is not allowed."); - Messages.Commands.Verification.Status status = new Messages.Commands.Verification.Status(); - status.setBody(List.of( - " ", - " Identica", - " Here you can see all enrolled 2FA methods", - " and to which providers they are assigned.", - " ", - " Enrolled methods:", - "{enrollments}", - " ", - " Provider selections:", - "{selections}", - " " - )); - status.setEmptyEnrollments(" ▪ No enrolled methods"); - status.setEmptySelections(" ▪ No provider selections"); - - Messages.Commands.Verification.Enroll enroll = new Messages.Commands.Verification.Enroll(); - enroll.setUnknownMethod("{prefix}Unknown verification method {methodDisplayName}."); - enroll.setAlreadyEnrolled("{prefix}Method {methodDisplayName} is already enrolled."); - - Messages.Commands.Verification.Confirm confirm = new Messages.Commands.Verification.Confirm(); - confirm.setNoPending("{prefix}No pending verification action."); - confirm.setInvalidCode("{prefix}Invalid verification code."); - confirm.setProtectedActionSelectionRequired("{prefix}Select a verification method for your current provider before continuing."); - confirm.setProtectedActionSessionRequired("{prefix}You must have an active session to confirm this protected action."); - confirm.setMethodUnavailable("{prefix}Method {methodDisplayName} is not available for {provider}."); - confirm.setEnabled("{prefix}Verification method {methodDisplayName} enabled."); - confirm.setAutoSelected("{prefix}Verification method {methodDisplayName} was automatically selected for {provider}."); - Messages.Commands.Verification.Confirm.RecoveryCodes recoveryCodes = new Messages.Commands.Verification.Confirm.RecoveryCodes(); - recoveryCodes.setLayout(Messages.Commands.Verification.Confirm.RecoveryCodes.Layout.TWO_COLUMN); - recoveryCodes.setBody(List.of( - " ", - " Identica", - " Use these recovery codes if you lose access", - " to your authenticator application.", - " ", - " Store them in a safe place before continuing:", - "{entries}", - " ", - " [CONFIRM] " + - "[CANCEL]", - " " - )); - Messages.Commands.EntryFormat recoveryEntry = new Messages.Commands.EntryFormat(); - recoveryEntry.setFormat(" {code}"); - recoveryEntry.setEmptyFormat(" {code}"); - recoveryCodes.setSingleColumnEntry(recoveryEntry); - Messages.Commands.EntryFormat twoColumnRecoveryEntry = new Messages.Commands.EntryFormat(); - twoColumnRecoveryEntry.setFormat(" {left} {right}"); - twoColumnRecoveryEntry.setEmptyFormat(" {left}"); - recoveryCodes.setTwoColumnEntry(twoColumnRecoveryEntry); - recoveryCodes.setEmpty(" No recovery codes generated"); - confirm.setRecoveryCodes(recoveryCodes); - - Messages.Commands.EntryFormat enrollmentEntry = new Messages.Commands.EntryFormat(); - enrollmentEntry.setFormat(" ▪ {methodDisplayName} [{enabledAt}]"); - enrollmentEntry.setEmptyFormat(" ▪ {methodDisplayName}"); - status.setEnrollmentEntry(enrollmentEntry); - - Messages.Commands.EntryFormat selectionEntry = new Messages.Commands.EntryFormat(); - selectionEntry.setFormat(" ▪ {provider}: {methodDisplayName}"); - selectionEntry.setEmptyFormat(" ▪ {provider}"); - status.setSelectionEntry(selectionEntry); - - Messages.Commands.Verification.Use use = new Messages.Commands.Verification.Use(); - use.setProviderNotFound("{prefix}Unknown provider {provider}."); - use.setProviderUnsupported("{prefix}Provider {provider} does not support verification."); - use.setProviderVerificationDisabled("{prefix}Verification is disabled for {provider}."); - use.setMethodNotEnrolled("{prefix}Method {methodDisplayName} is not enrolled."); - use.setMethodDisabledForProvider("{prefix}Method {methodDisplayName} is not enabled for {provider}."); - use.setAlreadySelected("{prefix}{methodDisplayName} is already selected for {provider}."); - use.setUpdated("{prefix}{methodDisplayName} selected for {provider}."); - - Messages.Commands.Verification.Disable disable = new Messages.Commands.Verification.Disable(); - disable.setMethodNotEnrolled("{prefix}Method {methodDisplayName} is not enrolled."); - disable.setProtectedPrompt("{prefix}Confirm the code from your authenticator with /2fa confirm [Code] to disable {methodDisplayName}."); - disable.setDisabled("{prefix}Verification method {methodDisplayName} disabled."); - - Messages.Commands.Verification.Cancel cancel = new Messages.Commands.Verification.Cancel(); - cancel.setNoPending("{prefix}No pending verification action."); - cancel.setCancelled("{prefix}Pending verification enrollment cancelled."); - cancel.setCancelledProtectedAction("{prefix}Pending protected action cancelled."); - - Messages.Commands.Verification.Methods.Totp totp = new Messages.Commands.Verification.Methods.Totp(); - totp.setPending(List.of( - " ", - " Identica", - " ", - " Scan or enter this TOTP secret:", - " {secret}", - " ", - " [OPEN QR CODE]", - " ", - " Use /2fa enroll confirm [Code] to continue.", - " " - )); - Messages.Commands.Verification.Methods methods = new Messages.Commands.Verification.Methods(); - methods.setTotp(totp); - - verification.setMethods(methods); - verification.setStatus(status); - verification.setEnroll(enroll); - verification.setConfirm(confirm); - verification.setUse(use); - verification.setDisable(disable); - verification.setCancel(cancel); - commands.setVerification(verification); - } } diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/config/defaults/messages/MessagesDefaults.java b/identica-common/src/main/java/me/whereareiam/identica/common/config/defaults/messages/MessagesDefaults.java index cba0cdea6..cdc3c6830 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/config/defaults/messages/MessagesDefaults.java +++ b/identica-common/src/main/java/me/whereareiam/identica/common/config/defaults/messages/MessagesDefaults.java @@ -55,7 +55,6 @@ private void applyProviders(Messages messages) { "", "discord.arcadeya.com" )); - providers.setProviderRestriction(buildProviderRestriction()); messages.setProviders(providers); } @@ -113,14 +112,6 @@ private Messages.Engine.Prepare buildPrepare() { "", "discord.arcadeya.com" )); - prepare.setProviderRestricted(List.of( - "ɪᴅᴇɴᴛɪᴄᴀ", - "", - "{providerName} is not accepting joins right now.", - "Please contact a server administrator.", - "", - "discord.arcadeya.com" - )); Messages.Engine.Prepare.Errors errors = new Messages.Engine.Prepare.Errors(); errors.setPreparePolicyMissing(List.of( "ɪᴅᴇɴᴛɪᴄᴀ", @@ -134,20 +125,6 @@ private Messages.Engine.Prepare buildPrepare() { return prepare; } - private Messages.Providers.ProviderRestriction buildProviderRestriction() { - Messages.Providers.ProviderRestriction providerRestriction = new Messages.Providers.ProviderRestriction(); - providerRestriction.setDenied(List.of( - "ɪᴅᴇɴᴛɪᴄᴀ", - "", - "{providerName} is temporarily unavailable.", - "Allowed joins currently require: {allow}.", - "If you think this is a mistake, contact a server administrator.", - "", - "discord.arcadeya.com" - )); - return providerRestriction; - } - private Messages.Scenarios.Authentication buildAuthentication() { Messages.Scenarios.Authentication authentication = new Messages.Scenarios.Authentication(); applyScenario(authentication, "Authentication"); diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/config/defaults/provider/ConflictsDefaults.java b/identica-common/src/main/java/me/whereareiam/identica/common/config/defaults/provider/ConflictsDefaults.java index ee26b60ac..6b0c1e52b 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/config/defaults/provider/ConflictsDefaults.java +++ b/identica-common/src/main/java/me/whereareiam/identica/common/config/defaults/provider/ConflictsDefaults.java @@ -17,10 +17,15 @@ public Conflicts supply(Conflicts config) { defaultRule.setResolvers(List.of(formatResolver("{username}*"))); usernameRules.setDefaultRule(defaultRule); + ObjectNode when = JsonNodeFactory.instance.objectNode(); + when.putArray("providers") + .add("premium") + .add("credential"); + Conflicts.ConflictRules.ConflictRule premiumVsCredential = new Conflicts.ConflictRules.ConflictRule(); - premiumVsCredential.setProviders(List.of("premium", "credential")); + premiumVsCredential.setWhen(when); premiumVsCredential.setResolvers(List.of(formatResolver("{username}_{incomingProvider}"))); - usernameRules.setPairs(List.of(premiumVsCredential)); + usernameRules.setCases(List.of(premiumVsCredential)); config.getRules().put("username", usernameRules); return config; diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/config/defaults/provider/ProvidersDefaults.java b/identica-common/src/main/java/me/whereareiam/identica/common/config/defaults/provider/ProvidersDefaults.java index 9ee00d3fd..7a36dcc8b 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/config/defaults/provider/ProvidersDefaults.java +++ b/identica-common/src/main/java/me/whereareiam/identica/common/config/defaults/provider/ProvidersDefaults.java @@ -3,8 +3,6 @@ import com.google.inject.Singleton; import me.whereareiam.configura.merge.defaults.DefaultsProvider; import me.whereareiam.identica.model.config.provider.Providers; -import me.whereareiam.identica.type.provider.ProviderJoinRestrictionCondition; -import me.whereareiam.identica.type.verification.UnavailableSelectionPolicy; import java.time.Duration; import java.util.List; @@ -15,7 +13,6 @@ public class ProvidersDefaults implements DefaultsProvider { public Providers supply(Providers config) { Providers.Behavior behavior = new Providers.Behavior(); behavior.setAttemptTtl(Duration.ofMinutes(10)); - behavior.setJoinRestrictionToggleTtl(Duration.ofDays(365)); config.setBehavior(behavior); Providers.ProviderEntry credential = new Providers.ProviderEntry(); @@ -24,11 +21,6 @@ public Providers supply(Providers config) { credential.setEnabled(true); credential.setPriority(50); credential.setEntrypoints(List.of("credential.arcadeya.com")); - credential.setRestriction(restriction( - ProviderJoinRestrictionCondition.RECOGNIZED, - ProviderJoinRestrictionCondition.LINKED - )); - credential.setVerification(credentialVerification()); Providers.ProviderEntry premium = new Providers.ProviderEntry(); premium.setId("premium"); @@ -36,56 +28,8 @@ public Providers supply(Providers config) { premium.setEnabled(true); premium.setPriority(100); premium.setEntrypoints(List.of("premium.arcadeya.com")); - premium.setRestriction(restriction(ProviderJoinRestrictionCondition.RECOGNIZED)); - premium.setVerification(premiumVerification()); config.setProviders(List.of(credential, premium)); return config; } - private Providers.ProviderEntry.Verification credentialVerification() { - Providers.ProviderEntry.Verification verification = new Providers.ProviderEntry.Verification(); - verification.setEnabled(true); - verification.setRequired(false); - verification.setUnavailableSelectionPolicy(UnavailableSelectionPolicy.KEEP_LOCKED); - - Providers.ProviderEntry.Verification.MethodEntry totp = - new Providers.ProviderEntry.Verification.MethodEntry(); - totp.setId("totp"); - totp.setEnabled(true); - totp.setPriority(100); - totp.setUnavailableSelectionPolicy(UnavailableSelectionPolicy.KEEP_LOCKED); - verification.setMethods(List.of(totp)); - - return verification; - } - - private Providers.ProviderEntry.Verification premiumVerification() { - Providers.ProviderEntry.Verification verification = new Providers.ProviderEntry.Verification(); - verification.setEnabled(true); - verification.setRequired(false); - verification.setUnavailableSelectionPolicy(UnavailableSelectionPolicy.KEEP_LOCKED); - - Providers.ProviderEntry.Verification.MethodEntry totp = - new Providers.ProviderEntry.Verification.MethodEntry(); - totp.setId("totp"); - totp.setEnabled(true); - totp.setPriority(100); - totp.setUnavailableSelectionPolicy(UnavailableSelectionPolicy.KEEP_LOCKED); - verification.setMethods(List.of(totp)); - - return verification; - } - - private Providers.ProviderEntry.Restriction restriction(ProviderJoinRestrictionCondition... allow) { - Providers.ProviderEntry.Restriction restriction = new Providers.ProviderEntry.Restriction(); - restriction.setJoin(joinRestriction(allow)); - return restriction; - } - - private Providers.ProviderEntry.Restriction.Join joinRestriction(ProviderJoinRestrictionCondition... allow) { - Providers.ProviderEntry.Restriction.Join restriction = new Providers.ProviderEntry.Restriction.Join(); - restriction.setEnabled(false); - restriction.setAllow(List.of(allow)); - return restriction; - } } diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/config/deserializer/ProviderCapabilityDeserializer.java b/identica-common/src/main/java/me/whereareiam/identica/common/config/deserializer/ProviderCapabilityDeserializer.java deleted file mode 100644 index ae6bda597..000000000 --- a/identica-common/src/main/java/me/whereareiam/identica/common/config/deserializer/ProviderCapabilityDeserializer.java +++ /dev/null @@ -1,23 +0,0 @@ -package me.whereareiam.identica.common.config.deserializer; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer; -import me.whereareiam.identica.type.provider.ProviderCapability; - -import java.io.IOException; - -public final class ProviderCapabilityDeserializer extends StdScalarDeserializer { - public ProviderCapabilityDeserializer() { - super(ProviderCapability.class); - } - - @Override - public ProviderCapability deserialize(JsonParser parser, DeserializationContext context) throws IOException { - String value = parser.getValueAsString(); - ProviderCapability capability = ProviderCapability.fromId(value); - - if (capability == null) throw new IllegalArgumentException("Unknown provider capability: " + value); - return capability; - } -} diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/config/provider/VerificationProvider.java b/identica-common/src/main/java/me/whereareiam/identica/common/config/provider/VerificationProvider.java deleted file mode 100644 index 3b0509799..000000000 --- a/identica-common/src/main/java/me/whereareiam/identica/common/config/provider/VerificationProvider.java +++ /dev/null @@ -1,30 +0,0 @@ -package me.whereareiam.identica.common.config.provider; - -import com.google.inject.Inject; -import com.google.inject.Singleton; -import com.google.inject.name.Named; -import me.whereareiam.configura.Config; -import me.whereareiam.configura.Configura; -import me.whereareiam.identica.Registry; -import me.whereareiam.identica.Reloadable; -import me.whereareiam.identica.common.config.defaults.VerificationDefaults; -import me.whereareiam.identica.config.ConfigProvider; -import me.whereareiam.identica.model.config.Verification; - -import java.nio.file.Path; - -@Singleton -public class VerificationProvider extends ConfigProvider { - @Inject - public VerificationProvider( - @Named("dataPath") Path dataPath, - Registry registry - ) { - super(dataPath, "verification", Verification.class, registry); - } - - @Override - protected Configura configura() { - return versioned(Config.configured().withDefaults(VerificationDefaults.class), Verification.class); - } -} diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/config/serializer/ProviderCapabilitySerializer.java b/identica-common/src/main/java/me/whereareiam/identica/common/config/serializer/ProviderCapabilitySerializer.java deleted file mode 100644 index 474abe8ed..000000000 --- a/identica-common/src/main/java/me/whereareiam/identica/common/config/serializer/ProviderCapabilitySerializer.java +++ /dev/null @@ -1,24 +0,0 @@ -package me.whereareiam.identica.common.config.serializer; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer; -import me.whereareiam.identica.type.provider.ProviderCapability; - -import java.io.IOException; - -public final class ProviderCapabilitySerializer extends StdScalarSerializer { - public ProviderCapabilitySerializer() { - super(ProviderCapability.class); - } - - @Override - public void serialize(ProviderCapability value, JsonGenerator gen, SerializerProvider provider) throws IOException { - if (value == null) { - gen.writeNull(); - return; - } - - gen.writeString(value.getId()); - } -} diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/conflict/ConflictConfiguration.java b/identica-common/src/main/java/me/whereareiam/identica/common/conflict/ConflictConfiguration.java index 863315c97..ee15f65c6 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/conflict/ConflictConfiguration.java +++ b/identica-common/src/main/java/me/whereareiam/identica/common/conflict/ConflictConfiguration.java @@ -2,7 +2,6 @@ import com.google.inject.AbstractModule; import com.google.inject.multibindings.Multibinder; -import me.whereareiam.identica.common.conflict.type.UsernameConflictType; import me.whereareiam.identica.conflict.ConflictGuard; import me.whereareiam.identica.conflict.ConflictService; @@ -11,6 +10,5 @@ public class ConflictConfiguration extends AbstractModule { protected void configure() { Multibinder.newSetBinder(binder(), ConflictGuard.class); bind(ConflictService.class).to(DefaultConflictService.class).asEagerSingleton(); - bind(UsernameConflictType.class).asEagerSingleton(); } } diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/conflict/ConflictPrepareLifecycle.java b/identica-common/src/main/java/me/whereareiam/identica/common/conflict/ConflictPrepareLifecycle.java deleted file mode 100644 index 42c1ab76d..000000000 --- a/identica-common/src/main/java/me/whereareiam/identica/common/conflict/ConflictPrepareLifecycle.java +++ /dev/null @@ -1,48 +0,0 @@ -package me.whereareiam.identica.common.conflict; - -import com.google.inject.Inject; -import com.google.inject.Singleton; -import me.whereareiam.identica.event.EventListener; -import me.whereareiam.identica.event.EventManager; -import me.whereareiam.identica.event.account.AccountPrepareEvent; -import me.whereareiam.identica.event.base.IdenticEvent; -import me.whereareiam.identica.model.conflict.ConflictContext; -import me.whereareiam.identica.type.ConflictHook; -import me.whereareiam.identica.model.conflict.ConflictResolution; -import me.whereareiam.identica.conflict.ConflictService; -import me.whereareiam.identica.conflict.ConflictType; -import me.whereareiam.identica.type.event.EventOrder; -import org.jetbrains.annotations.NotNull; - -@Singleton -public class ConflictPrepareLifecycle implements EventListener { - private final @NotNull ConflictService conflictService; - - @Inject - public ConflictPrepareLifecycle( - @NotNull ConflictService conflictService, - @NotNull EventManager eventManager - ) { - this.conflictService = conflictService; - eventManager.register(this); - } - - @IdenticEvent(EventOrder.HIGHEST) - public void onPrepare(@NotNull AccountPrepareEvent event) { - if (event.getDecision() != null) return; - - for (ConflictType type : conflictService.getTypes()) { - if (event.getDecision() != null) return; - if (type.getDefaultHook() != ConflictHook.PREPARE) - continue; - - ConflictContext context = type.createContext(event); - if (context == null) continue; - - ConflictResolution resolution = conflictService.resolve(context); - if (resolution == null) continue; - - type.apply(event, context, resolution); - } - } -} diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/conflict/DefaultConflictService.java b/identica-common/src/main/java/me/whereareiam/identica/common/conflict/DefaultConflictService.java index eb4021ec0..cb51731a1 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/conflict/DefaultConflictService.java +++ b/identica-common/src/main/java/me/whereareiam/identica/common/conflict/DefaultConflictService.java @@ -8,48 +8,37 @@ import com.google.inject.Singleton; import me.whereareiam.identica.common.util.UniqueIdResolutionSupport; import me.whereareiam.identica.conflict.ConflictGuard; -import me.whereareiam.identica.model.conflict.ConflictContext; -import me.whereareiam.identica.model.conflict.ConflictResolution; -import me.whereareiam.identica.model.config.provider.Conflicts; -import me.whereareiam.identica.model.config.provider.Conflicts.ConflictRules.ConflictRule; -import me.whereareiam.identica.model.config.provider.Conflicts.ConflictRules.ConflictRule.ResolverEntry; -import me.whereareiam.identica.model.identity.provider.AccountProviderLink; -import me.whereareiam.identica.common.conflict.resolver.defaults.KickActiveConflictResolver; -import me.whereareiam.identica.common.conflict.resolver.defaults.KickBothConflictResolver; -import me.whereareiam.identica.common.conflict.resolver.defaults.KickJoinerConflictResolver; -import me.whereareiam.identica.conflict.resolver.ConflictResolver; import me.whereareiam.identica.conflict.ConflictService; import me.whereareiam.identica.conflict.ConflictType; +import me.whereareiam.identica.conflict.resolver.ConflictResolver; import me.whereareiam.identica.logging.Logger; +import me.whereareiam.identica.model.config.provider.Conflicts; +import me.whereareiam.identica.model.config.provider.Conflicts.ConflictRules.ConflictRule; +import me.whereareiam.identica.model.config.provider.Conflicts.ConflictRules.ConflictRule.ResolverEntry; +import me.whereareiam.identica.model.conflict.ConflictContext; +import me.whereareiam.identica.model.conflict.ConflictResolution; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.List; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; @Singleton public class DefaultConflictService implements ConflictService { private final Provider conflictsConfig; private final Set globalGuards; private final Map resolvers = new ConcurrentHashMap<>(); - private final Map types = new ConcurrentHashMap<>(); + private final Map> types = new ConcurrentHashMap<>(); @Inject public DefaultConflictService( Provider conflictsConfig, - Set globalGuards, - KickJoinerConflictResolver kickJoinerResolver, - KickActiveConflictResolver kickActiveResolver, - KickBothConflictResolver kickBothResolver + Set globalGuards ) { this.conflictsConfig = conflictsConfig; this.globalGuards = globalGuards; - - register(kickJoinerResolver); - register(kickActiveResolver); - register(kickBothResolver); } @Override @@ -74,7 +63,7 @@ public void unregister(@NotNull ConflictResolver resolver) { } @Override - public void register(@NotNull ConflictType type) { + public void register(@NotNull ConflictType type) { String key = UniqueIdResolutionSupport.normalize(type.getKey()); if (key == null) return; types.put(key, type); @@ -83,7 +72,7 @@ public void register(@NotNull ConflictType type) { } @Override - public void unregister(@NotNull ConflictType type) { + public void unregister(@NotNull ConflictType type) { String key = UniqueIdResolutionSupport.normalize(type.getKey()); if (key == null) return; types.remove(key); @@ -92,14 +81,14 @@ public void unregister(@NotNull ConflictType type) { } @Override - public @Nullable ConflictType getType(@NotNull String key) { + public @Nullable ConflictType getType(@NotNull String key) { String normalized = UniqueIdResolutionSupport.normalize(key); if (normalized == null) return null; return types.get(normalized); } @Override - public @NotNull Set getTypes() { + public @NotNull Set> getTypes() { return Set.copyOf(types.values()); } @@ -108,19 +97,19 @@ public void unregister(@NotNull ConflictType type) { Conflicts.ConflictRules rules = resolveRules(context); if (rules == null) return null; - ConflictType type = getType(context.getKey()); + ConflictType type = getType(context.getKey()); ConflictResolution guardResolution = applyGuards(context, type); if (guardResolution != null) return guardResolution; - ConflictRule pairRule = resolvePairRule(rules, context); - if (pairRule != null) { - ConflictResolution resolution = resolveRule(context, pairRule); - if (resolution.getAction() != ConflictResolution.Action.PASS) return resolution; + ConflictRule caseRule = resolveCaseRule(rules, type, context); + if (caseRule != null) { + ConflictResolution resolution = resolveRule(context, caseRule); + if (resolution.getDecision() != ConflictResolution.Decision.PASS) return resolution; } ConflictRule defaultRule = rules.getDefaultRule(); ConflictResolution resolution = resolveRule(context, defaultRule); - if (resolution.getAction() == ConflictResolution.Action.PASS) { + if (resolution.getDecision() == ConflictResolution.Decision.PASS) { Logger.warn("Conflict resolution passed for key: %s", context.getKey()); return ConflictResolution.allow(); } @@ -135,45 +124,25 @@ public void unregister(@NotNull ConflictType type) { return rules.get(context.getKey()); } - private @Nullable ConflictRule resolvePairRule( + private @Nullable ConflictRule resolveCaseRule( @NotNull Conflicts.ConflictRules rules, + @Nullable ConflictType type, @NotNull ConflictContext context ) { - List pairs = rules.getPairs(); - if (pairs.isEmpty()) return null; + List cases = rules.getCases(); + if (type == null || cases.isEmpty()) return null; - for (ConflictRule rule : pairs) { - if (matchesProviders(rule, context)) + for (ConflictRule rule : cases) { + if (rule != null && type.matchesRule(context, rule.getWhen())) return rule; } - return null; - } - - private boolean matchesProviders(ConflictRule rule, ConflictContext context) { - if (rule == null || rule.getProviders().isEmpty()) - return false; - AccountProviderLink incoming = context.getIncomingLink(); - AccountProviderLink existing = context.getExistingLink(); - if (incoming == null || existing == null) return false; - - String incomingProvider = incoming.getProviderId(); - String existingProvider = existing.getProviderId(); - - boolean hasIncoming = false; - boolean hasExisting = false; - for (String provider : rule.getProviders()) { - if (provider == null || provider.isBlank()) continue; - if (provider.equalsIgnoreCase(incomingProvider)) hasIncoming = true; - if (provider.equalsIgnoreCase(existingProvider)) hasExisting = true; - } - - return hasIncoming && hasExisting; + return null; } private @Nullable ConflictResolution applyGuards( @NotNull ConflictContext context, - @Nullable ConflictType type + @Nullable ConflictType type ) { for (ConflictGuard guard : globalGuards) { if (guard == null) continue; @@ -214,7 +183,7 @@ private boolean matchesProviders(ConflictRule rule, ConflictContext context) { JsonNode params = resolveParams(entry); ConflictResolution resolution = resolver.resolve(context, params); - if (resolution.getAction() == ConflictResolution.Action.PASS) + if (resolution.getDecision() == ConflictResolution.Decision.PASS) continue; return resolution; diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/conflict/resolver/defaults/KickActiveConflictResolver.java b/identica-common/src/main/java/me/whereareiam/identica/common/conflict/resolver/defaults/KickActiveConflictResolver.java deleted file mode 100644 index 717746edf..000000000 --- a/identica-common/src/main/java/me/whereareiam/identica/common/conflict/resolver/defaults/KickActiveConflictResolver.java +++ /dev/null @@ -1,24 +0,0 @@ -package me.whereareiam.identica.common.conflict.resolver.defaults; - -import com.fasterxml.jackson.databind.JsonNode; -import com.google.inject.Singleton; -import me.whereareiam.identica.model.conflict.ConflictContext; -import me.whereareiam.identica.model.conflict.ConflictResolution; -import me.whereareiam.identica.conflict.resolver.ConflictResolver; -import org.jetbrains.annotations.NotNull; - -@Singleton -public class KickActiveConflictResolver implements ConflictResolver { - @Override - public @NotNull String getId() { - return "kick_active"; - } - - @Override - public @NotNull ConflictResolution resolve( - @NotNull ConflictContext context, - @NotNull JsonNode params - ) { - return ConflictResolution.kickExisting(null); - } -} diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/conflict/resolver/defaults/KickBothConflictResolver.java b/identica-common/src/main/java/me/whereareiam/identica/common/conflict/resolver/defaults/KickBothConflictResolver.java deleted file mode 100644 index 61a0a11d6..000000000 --- a/identica-common/src/main/java/me/whereareiam/identica/common/conflict/resolver/defaults/KickBothConflictResolver.java +++ /dev/null @@ -1,24 +0,0 @@ -package me.whereareiam.identica.common.conflict.resolver.defaults; - -import com.fasterxml.jackson.databind.JsonNode; -import com.google.inject.Singleton; -import me.whereareiam.identica.model.conflict.ConflictContext; -import me.whereareiam.identica.model.conflict.ConflictResolution; -import me.whereareiam.identica.conflict.resolver.ConflictResolver; -import org.jetbrains.annotations.NotNull; - -@Singleton -public class KickBothConflictResolver implements ConflictResolver { - @Override - public @NotNull String getId() { - return "kick_both"; - } - - @Override - public @NotNull ConflictResolution resolve( - @NotNull ConflictContext context, - @NotNull JsonNode params - ) { - return ConflictResolution.kickBoth(null); - } -} diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/conflict/resolver/defaults/KickJoinerConflictResolver.java b/identica-common/src/main/java/me/whereareiam/identica/common/conflict/resolver/defaults/KickJoinerConflictResolver.java deleted file mode 100644 index 1e1082f00..000000000 --- a/identica-common/src/main/java/me/whereareiam/identica/common/conflict/resolver/defaults/KickJoinerConflictResolver.java +++ /dev/null @@ -1,24 +0,0 @@ -package me.whereareiam.identica.common.conflict.resolver.defaults; - -import com.fasterxml.jackson.databind.JsonNode; -import com.google.inject.Singleton; -import me.whereareiam.identica.model.conflict.ConflictContext; -import me.whereareiam.identica.model.conflict.ConflictResolution; -import me.whereareiam.identica.conflict.resolver.ConflictResolver; -import org.jetbrains.annotations.NotNull; - -@Singleton -public class KickJoinerConflictResolver implements ConflictResolver { - @Override - public @NotNull String getId() { - return "kick_joiner"; - } - - @Override - public @NotNull ConflictResolution resolve( - @NotNull ConflictContext context, - @NotNull JsonNode params - ) { - return ConflictResolution.deny(null); - } -} diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/conflict/type/UsernameConflictType.java b/identica-common/src/main/java/me/whereareiam/identica/common/conflict/type/UsernameConflictType.java deleted file mode 100644 index 8747c2e48..000000000 --- a/identica-common/src/main/java/me/whereareiam/identica/common/conflict/type/UsernameConflictType.java +++ /dev/null @@ -1,222 +0,0 @@ -package me.whereareiam.identica.common.conflict.type; - -import com.google.inject.Inject; -import com.google.inject.Singleton; -import me.whereareiam.identica.common.conflict.guard.UsernameEntrypointConflictGuard; -import me.whereareiam.identica.common.conflict.resolver.username.FormatUsernameConflictResolver; -import me.whereareiam.identica.conflict.ConflictService; -import me.whereareiam.identica.conflict.ConflictType; -import me.whereareiam.identica.conflict.ConflictGuard; -import me.whereareiam.identica.conflict.resolver.ConflictResolver; -import me.whereareiam.identica.database.AccountPersistenceService; -import me.whereareiam.identica.database.provider.ProviderLinkPersistenceService; -import me.whereareiam.identica.database.provider.ProviderProfilePersistenceService; -import me.whereareiam.identica.event.account.AccountPrepareEvent; -import me.whereareiam.identica.model.Session; -import me.whereareiam.identica.model.identity.Account; -import me.whereareiam.identica.model.identity.AccountDecision; -import me.whereareiam.identica.model.conflict.ConflictContext; -import me.whereareiam.identica.model.conflict.ConflictResolution; -import me.whereareiam.identica.model.identity.provider.AccountProviderLink; -import me.whereareiam.identica.model.identity.provider.AccountProviderProfile; -import me.whereareiam.identica.identity.session.SessionService; -import me.whereareiam.identica.model.provider.ProviderContext; -import me.whereareiam.identica.type.provider.ProviderOrigin; -import me.whereareiam.identica.type.ConflictHook; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.List; -import java.util.Optional; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; - -@Singleton -public class UsernameConflictType implements ConflictType { - private static final String KEY = "username"; - - private final AccountPersistenceService accountPersistenceService; - private final ProviderLinkPersistenceService providerLinkPersistenceService; - private final ProviderProfilePersistenceService providerProfilePersistenceService; - private final SessionService sessionService; - private final FormatUsernameConflictResolver formatResolver; - private final UsernameEntrypointConflictGuard entrypointGuard; - - @Inject - public UsernameConflictType( - AccountPersistenceService accountPersistenceService, - ProviderLinkPersistenceService providerLinkPersistenceService, - ProviderProfilePersistenceService providerProfilePersistenceService, - SessionService sessionService, - FormatUsernameConflictResolver formatResolver, - UsernameEntrypointConflictGuard entrypointGuard, - ConflictService conflictService - ) { - this.accountPersistenceService = accountPersistenceService; - this.providerLinkPersistenceService = providerLinkPersistenceService; - this.providerProfilePersistenceService = providerProfilePersistenceService; - this.sessionService = sessionService; - this.formatResolver = formatResolver; - this.entrypointGuard = entrypointGuard; - - conflictService.register(this); - } - - @Override - public @NotNull String getKey() { - return KEY; - } - - @Override - public @NotNull ConflictHook getDefaultHook() { - return ConflictHook.PREPARE; - } - - @Override - public @NotNull List getResolvers() { - return List.of(formatResolver); - } - - @Override - public @NotNull List getGuards() { - return List.of(entrypointGuard); - } - - @Override - public @Nullable ConflictContext createContext(@NotNull AccountPrepareEvent event) { - String candidate = event.getEffectiveUsername(); - if (candidate == null || candidate.isBlank()) - return null; - - Account account = event.getAccount(); - AccountProviderLink incomingLink = event.getLink(); - - candidate = candidate.trim(); - Account conflictAccount = resolveActiveConflict(account.getUniqueId(), candidate).orElse(null); - if (conflictAccount == null) return null; - - AccountProviderLink existingLink = resolvePrimaryLink(conflictAccount.getUniqueId()).orElse(null); - AccountProviderProfile existingProfile = existingLink != null - ? providerProfilePersistenceService.findBySubject( - existingLink.getProviderId(), - existingLink.getProviderSubject() - ).orElse(null) - : null; - - if (existingLink == null) - return null; - - ConflictContext context = ConflictContext.builder() - .key(KEY) - .candidate(candidate) - .incomingAccount(account) - .incomingLink(incomingLink) - .existingAccount(conflictAccount) - .existingLink(existingLink) - .existingProfile(existingProfile) - .build(); - - ProviderContext providerContext = event.getProvider(); - ProviderOrigin source = providerContext != null ? providerContext.getSource() : null; - if (source != null) context.putExtra("entrypointSource", source); - - resolvePrimaryLink(account.getUniqueId()).ifPresent(primaryIncoming -> - context.putExtra("incomingPrimaryProviderId", primaryIncoming.getProviderId()) - ); - - return context; - } - - @Override - public void apply( - @NotNull AccountPrepareEvent event, - @NotNull ConflictContext context, - @NotNull ConflictResolution resolution - ) { - if (resolution.getAction() == ConflictResolution.Action.DENY) { - event.setDecision(AccountDecision.deny(resolution.getMessage())); - return; - } - - Account existing = context.getExistingAccount(); - if (existing != null && resolution.getAction() == ConflictResolution.Action.KICK_EXISTING) - sessionService.close(existing.getUniqueId()).join(); - - if (existing != null && resolution.getAction() == ConflictResolution.Action.KICK_BOTH) { - sessionService.close(existing.getUniqueId()).join(); - event.setDecision(AccountDecision.deny(resolution.getMessage())); - return; - } - - String overrideValue = resolution.getOverrideValue(); - if (overrideValue != null && !overrideValue.isBlank()) - applyOverride(event, context, resolution); - - event.setDecision(AccountDecision.allow()); - } - - private void applyOverride( - @NotNull AccountPrepareEvent event, - @NotNull ConflictContext context, - @NotNull ConflictResolution resolution - ) { - String overrideValue = resolution.getOverrideValue(); - if (overrideValue == null || overrideValue.isBlank()) return; - - ConflictResolution.OverrideTarget target = resolution.getOverrideTarget(); - if (target == ConflictResolution.OverrideTarget.INCOMING - || target == ConflictResolution.OverrideTarget.BOTH) - event.setEffectiveUsername(overrideValue); - - if (target == ConflictResolution.OverrideTarget.EXISTING - || target == ConflictResolution.OverrideTarget.BOTH) - applyExistingOverride(context, overrideValue); - } - - private void applyExistingOverride(@NotNull ConflictContext context, @NotNull String overrideValue) { - Account existing = context.getExistingAccount(); - if (existing == null) return; - - UUID uniqueId = existing.getUniqueId(); - sessionService.findByUniqueId(uniqueId) - .thenCompose(found -> { - if (found.isEmpty()) - return CompletableFuture.completedFuture(null); - - Session session = found.get(); - session.setEffectiveUsername(overrideValue); - - return sessionService.open(session).thenApply(ignored -> null); - }).join(); - } - - private Optional resolveActiveConflict(@NotNull UUID uniqueId, @NotNull String username) { - List accounts = accountPersistenceService.findByUsername(username); - if (accounts.isEmpty()) return Optional.empty(); - - for (Account account : accounts) { - if (account == null) continue; - if (account.getUniqueId().equals(uniqueId)) continue; - if (isActive(account.getUniqueId())) return Optional.of(account); - } - - return Optional.empty(); - } - - private Optional resolvePrimaryLink(@NotNull UUID uniqueId) { - List links = providerLinkPersistenceService.findByUniqueId(uniqueId); - if (links.isEmpty()) return Optional.empty(); - - for (AccountProviderLink link : links) { - if (link != null && link.isPrimaryLink()) return Optional.of(link); - } - - return Optional.of(links.getFirst()); - } - - private boolean isActive(@NotNull UUID uniqueId) { - return sessionService.findByUniqueId(uniqueId) - .thenApply(Optional::isPresent) - .join(); - } -} diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/SessionConfiguration.java b/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/SessionConfiguration.java index dc5a3b26b..d0dee466a 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/SessionConfiguration.java +++ b/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/SessionConfiguration.java @@ -1,25 +1,11 @@ package me.whereareiam.identica.common.identity.session; import com.google.inject.AbstractModule; -import me.whereareiam.identica.common.identity.session.recognition.DefaultSessionRecognitionService; -import me.whereareiam.identica.common.identity.session.recognition.DefaultSessionRecognitionStore; -import me.whereareiam.identica.common.identity.session.recognition.RecognitionEligibilityBootstrap; -import me.whereareiam.identica.common.identity.session.recognition.eligibility.DefaultRecognitionEligibilityRegistry; -import me.whereareiam.identica.common.identity.session.recognition.eligibility.DefaultRecognitionEligibilityService; import me.whereareiam.identica.identity.session.SessionService; -import me.whereareiam.identica.identity.session.recognition.SessionRecognitionService; -import me.whereareiam.identica.identity.session.recognition.SessionRecognitionStore; -import me.whereareiam.identica.identity.session.recognition.eligibility.RecognitionEligibilityRegistry; -import me.whereareiam.identica.identity.session.recognition.eligibility.RecognitionEligibilityService; public class SessionConfiguration extends AbstractModule { @Override protected void configure() { bind(SessionService.class).to(DefaultSessionService.class).asEagerSingleton(); - bind(SessionRecognitionStore.class).to(DefaultSessionRecognitionStore.class).asEagerSingleton(); - bind(SessionRecognitionService.class).to(DefaultSessionRecognitionService.class).asEagerSingleton(); - bind(RecognitionEligibilityRegistry.class).to(DefaultRecognitionEligibilityRegistry.class).asEagerSingleton(); - bind(RecognitionEligibilityService.class).to(DefaultRecognitionEligibilityService.class).asEagerSingleton(); - bind(RecognitionEligibilityBootstrap.class).asEagerSingleton(); } } diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/RecognitionEligibilityBootstrap.java b/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/RecognitionEligibilityBootstrap.java deleted file mode 100644 index 9f7d682ba..000000000 --- a/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/RecognitionEligibilityBootstrap.java +++ /dev/null @@ -1,23 +0,0 @@ -package me.whereareiam.identica.common.identity.session.recognition; - -import com.google.inject.Inject; -import com.google.inject.Singleton; -import me.whereareiam.identica.common.identity.session.recognition.eligibility.rule.ExplicitSelectionRecognitionEligibilityRule; -import me.whereareiam.identica.common.identity.session.recognition.eligibility.rule.RecognitionEnabledEligibilityRule; -import me.whereareiam.identica.common.identity.session.recognition.eligibility.rule.UntrustedIpRecognitionEligibilityRule; -import me.whereareiam.identica.identity.session.recognition.eligibility.RecognitionEligibilityRegistry; - -@Singleton -public class RecognitionEligibilityBootstrap { - @Inject - public RecognitionEligibilityBootstrap( - RecognitionEligibilityRegistry registry, - RecognitionEnabledEligibilityRule recognitionEnabledEligibilityRule, - ExplicitSelectionRecognitionEligibilityRule explicitSelectionRecognitionEligibilityRule, - UntrustedIpRecognitionEligibilityRule untrustedIpRecognitionEligibilityRule - ) { - registry.register(recognitionEnabledEligibilityRule); - registry.register(explicitSelectionRecognitionEligibilityRule); - registry.register(untrustedIpRecognitionEligibilityRule); - } -} diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/listener/ListenerConfiguration.java b/identica-common/src/main/java/me/whereareiam/identica/common/listener/ListenerConfiguration.java index fe35d1d12..f4b9a3b90 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/listener/ListenerConfiguration.java +++ b/identica-common/src/main/java/me/whereareiam/identica/common/listener/ListenerConfiguration.java @@ -1,7 +1,6 @@ package me.whereareiam.identica.common.listener; import com.google.inject.AbstractModule; -import me.whereareiam.identica.common.conflict.ConflictPrepareLifecycle; import me.whereareiam.identica.common.listener.session.SessionClosedDisconnectListener; import me.whereareiam.identica.common.listener.session.SessionReplacedListener; import me.whereareiam.identica.common.provider.ProviderEntrypointSelectionLifecycle; @@ -13,7 +12,6 @@ public class ListenerConfiguration extends AbstractModule { protected void configure() { bind(SessionClosedDisconnectListener.class).asEagerSingleton(); bind(SessionReplacedListener.class).asEagerSingleton(); - bind(ConflictPrepareLifecycle.class).asEagerSingleton(); bind(ProviderEntrypointSelectionLifecycle.class).asEagerSingleton(); bind(ConnectionAttemptSentinelLifecycle.class).asEagerSingleton(); bind(DynamicListenerRegistry.class).to(DefaultDynamicListenerRegistry.class).asEagerSingleton(); diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/messaging/DefaultDeliveryService.java b/identica-common/src/main/java/me/whereareiam/identica/common/messaging/DefaultDeliveryService.java index c415f2549..4186342f5 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/messaging/DefaultDeliveryService.java +++ b/identica-common/src/main/java/me/whereareiam/identica/common/messaging/DefaultDeliveryService.java @@ -4,12 +4,14 @@ import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; import me.whereareiam.identica.Serializer; -import me.whereareiam.identica.identity.IdentityService; import me.whereareiam.identica.identity.actor.Identity; import me.whereareiam.identica.logging.Logger; import me.whereareiam.identica.model.delivery.DeliveryDispatchContext; import me.whereareiam.identica.model.delivery.DeliveryPayload; import me.whereareiam.identica.model.delivery.DeliveryRequest; +import me.whereareiam.identica.model.pipeline.journey.JourneyStateItem; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.PipelineStateReference; import me.whereareiam.identica.pipeline.state.PipelineStateStore; import me.whereareiam.identica.service.DeliveryService; import me.whereareiam.identica.service.DeliveryStore; @@ -26,7 +28,6 @@ @RequiredArgsConstructor(onConstructor_ = @Inject) public class DefaultDeliveryService implements DeliveryService { private final DeliveryStore deliveryStore; - private final IdentityService identityService; private final PipelineStateStore pipelineStateStore; @Override @@ -150,14 +151,13 @@ private boolean isStillValid(@NotNull DeliveryRequest request, @NotNull Identity UUID connectionUniqueId = identity.getConnectionUniqueId(); if (connectionUniqueId == null) return false; - me.whereareiam.identica.model.pipeline.state.PipelineStateReference reference = - me.whereareiam.identica.model.pipeline.state.PipelineStateReference.builder() + PipelineStateReference reference = PipelineStateReference.builder() .connectionUniqueId(connectionUniqueId) .build(); - me.whereareiam.identica.model.pipeline.state.PipelineState state = pipelineStateStore.find(reference).orElse(null); + PipelineState state = pipelineStateStore.find(reference).orElse(null); if (state == null) return false; - me.whereareiam.identica.model.pipeline.journey.JourneyStateItem journey = state.item(me.whereareiam.identica.model.pipeline.journey.JourneyStateItem.class).orElse(null); + JourneyStateItem journey = state.item(JourneyStateItem.class).orElse(null); String stageId = journey != null ? journey.getStageId() : null; int stepIndex = journey != null ? journey.getStepIndex() : -1; @@ -165,16 +165,4 @@ private boolean isStillValid(@NotNull DeliveryRequest request, @NotNull Identity && stepIndex == request.getMarker().getStepIndex() && java.util.Objects.equals(stageId, request.getMarker().getStageId()); } - - private Identity resolveIdentity(@NotNull DeliveryRequest request) { - UUID connectionUniqueId = request.getTarget().getConnectionUniqueId(); - if (connectionUniqueId != null) - return identityService.findByConnectionUniqueId(connectionUniqueId).orElse(null); - - UUID accountUniqueId = request.getTarget().getAccountUniqueId(); - if (accountUniqueId != null) - return identityService.findByAccountUniqueId(accountUniqueId).orElse(null); - - return null; - } } diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/migration/DefaultMigrationService.java b/identica-common/src/main/java/me/whereareiam/identica/common/migration/DefaultMigrationService.java index aaadb84e9..03a1a6085 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/migration/DefaultMigrationService.java +++ b/identica-common/src/main/java/me/whereareiam/identica/common/migration/DefaultMigrationService.java @@ -8,7 +8,6 @@ import me.whereareiam.identica.database.AccountPersistenceService; import me.whereareiam.identica.database.provider.ProviderLinkPersistenceService; import me.whereareiam.identica.event.EventManager; -import me.whereareiam.identica.type.ScenarioResolution; import me.whereareiam.identica.event.scenario.migration.MigrationRequiredEvent; import me.whereareiam.identica.event.scenario.migration.MigrationResolvedEvent; import me.whereareiam.identica.identity.IdentityService; @@ -19,7 +18,7 @@ import me.whereareiam.identica.model.config.Commands; import me.whereareiam.identica.model.config.Engine; import me.whereareiam.identica.model.config.Messages; -import me.whereareiam.identica.model.config.Settings; +import me.whereareiam.identica.model.config.provider.Providers; import me.whereareiam.identica.model.delivery.DeliveryPayload; import me.whereareiam.identica.model.delivery.DeliveryRequest; import me.whereareiam.identica.model.delivery.DeliveryTarget; @@ -28,12 +27,13 @@ import me.whereareiam.identica.model.migration.MigrationContext; import me.whereareiam.identica.model.migration.PendingMigration; import me.whereareiam.identica.model.migration.operation.*; +import me.whereareiam.identica.model.pipeline.journey.JourneyPlan; import me.whereareiam.identica.model.pipeline.journey.JourneyStateItem; import me.whereareiam.identica.model.pipeline.migration.MigrationPendingState; -import me.whereareiam.identica.model.pipeline.state.PipelineState; -import me.whereareiam.identica.model.pipeline.state.PipelineStateReference; import me.whereareiam.identica.model.provider.InternalProvider; -import me.whereareiam.identica.model.provider.ProviderContext; +import me.whereareiam.identica.pipeline.journey.registry.type.MigrationJourneyRegistry; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.PipelineStateReference; import me.whereareiam.identica.pipeline.state.PipelineStateStore; import me.whereareiam.identica.provider.ProviderManager; import me.whereareiam.identica.provider.migration.MigrationPrecheckContext; @@ -41,6 +41,7 @@ import me.whereareiam.identica.provider.migration.ProviderMigrationPrecheck; import me.whereareiam.identica.service.DeliveryService; import me.whereareiam.identica.service.MigrationService; +import me.whereareiam.identica.type.ScenarioResolution; import me.whereareiam.identica.type.messaging.DeliveryCheckpoint; import me.whereareiam.identica.type.messaging.DeliverySemantics; import me.whereareiam.identica.type.messaging.DeliverySource; @@ -49,9 +50,6 @@ import me.whereareiam.identica.type.migration.MigrationResultStatus; import me.whereareiam.identica.type.pipeline.PipelineType; import me.whereareiam.identica.type.pipeline.journey.JourneyMode; -import me.whereareiam.identica.type.provider.ProviderCapability; -import me.whereareiam.identica.type.provider.ProviderOrigin; -import me.whereareiam.identica.util.UniqueIdGenerator; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -65,15 +63,20 @@ @RequiredArgsConstructor(onConstructor_ = @Inject) public class DefaultMigrationService implements MigrationService { private final ProviderManager providerManager; + private final MigrationJourneyRegistry migrationJourneyRegistry; + private final ProviderLinkPersistenceService providerLinkPersistenceService; private final AccountPersistenceService accountPersistenceService; private final PipelineStateStore pipelineStateStore; + private final SessionService sessionService; private final IdentityService identityService; + private final DeliveryService deliveryService; private final EventManager eventManager; - private final Provider settingsProvider; + private final Provider engineProvider; + private final Provider providersProvider; private final Provider commandsProvider; private final Provider messagesProvider; @@ -114,6 +117,8 @@ public class DefaultMigrationService implements MigrationService { request.getInitiatorUniqueId(), System.currentTimeMillis() ); + MigrationResult targetValidation = validateTargetProvider(pendingMigration); + if (targetValidation != null) return targetValidation; pending.put(connectionUniqueId, pendingMigration); return result(MigrationResultStatus.PENDING_CONFIRMATION, null); @@ -137,6 +142,11 @@ public class DefaultMigrationService implements MigrationService { pending.remove(connectionUniqueId); return result(MigrationResultStatus.PENDING_EXISTS, null); } + MigrationResult targetValidation = validateTargetProvider(pendingMigration); + if (targetValidation != null) { + pending.remove(connectionUniqueId); + return targetValidation; + } if (!isUsernameFree(pendingMigration.username(), pendingMigration.uniqueId())) { pending.remove(connectionUniqueId); @@ -196,6 +206,8 @@ public class DefaultMigrationService implements MigrationService { start.getInitiatorUniqueId(), System.currentTimeMillis() ); + MigrationResult targetValidation = validateTargetProvider(pendingMigration); + if (targetValidation != null) return targetValidation; if (!isUsernameFree(pendingMigration.username(), pendingMigration.uniqueId())) { return result(MigrationResultStatus.PRECHECK_DENIED, migrationLockedMessage()); @@ -270,10 +282,6 @@ private boolean storePendingMigration(@NotNull PendingConfirmationMigration pend )) .targetProviderId(pendingMigration.targetProviderId()) .build(); - context.setProvider(resolvePendingProviderContext( - pendingMigration.targetProviderId(), - pendingMigration.username() - )); PipelineState pipelineState = PipelineState.initial(); pipelineState.setPipelineType(PipelineType.MIGRATION); @@ -305,39 +313,22 @@ private boolean storePendingMigration(@NotNull PendingConfirmationMigration pend return true; } - private @Nullable ProviderContext resolvePendingProviderContext( - @Nullable String targetProviderId, - @Nullable String username - ) { - String normalizedProviderId = normalize(targetProviderId); - String normalizedUsername = normalize(username); - if (normalizedProviderId == null || normalizedUsername == null) - return null; - - InternalProvider targetProvider = resolveProvider(normalizedProviderId); - if (targetProvider == null || targetProvider.getDescriptor() == null) - return null; - - if (!targetProvider.getDescriptor().hasCapability(ProviderCapability.OFFLINE_MODE)) - return null; + private @Nullable MigrationResult validateTargetProvider(@NotNull PendingConfirmationMigration pendingMigration) { + String targetProviderId = pendingMigration.targetProviderId(); + if (targetProviderId == null || targetProviderId.isBlank()) + return result(MigrationResultStatus.FAILED, null); - UUID offlineUniqueId = UniqueIdGenerator.offlinePlayerUniqueId(normalizedUsername); - if (offlineUniqueId == null) - return null; + InternalProvider provider = resolveActiveProvider(targetProviderId); + if (provider == null) { + return isConfiguredProvider(targetProviderId) + ? result(MigrationResultStatus.PROVIDER_UNAVAILABLE, null) + : result(MigrationResultStatus.TARGET_UNSUPPORTED, null); + } + + if (!supportsMigrationJourney(targetProviderId, pendingMigration)) + return result(MigrationResultStatus.TARGET_UNSUPPORTED, null); - ProviderContext provider = ProviderContext.builder() - .providerId(normalizedProviderId) - .providerSubject(offlineUniqueId.toString()) - .providerUsername(normalizedUsername) - .source(ProviderOrigin.AUTO) - .build(); - Logger.debug( - "Prepared offline migration provider context target=%s username=%s subject=%s", - normalizedProviderId, - normalizedUsername, - offlineUniqueId - ); - return provider; + return null; } private boolean hasPendingMigration(@NotNull UUID connectionUniqueId) { @@ -345,7 +336,7 @@ private boolean hasPendingMigration(@NotNull UUID connectionUniqueId) { } private MigrationPrecheckResult runPrechecks(@NotNull PendingConfirmationMigration pendingMigration) { - InternalProvider provider = resolveProvider(pendingMigration.targetProviderId()); + InternalProvider provider = resolveActiveProvider(pendingMigration.targetProviderId()); Set prechecks = provider != null ? provider.getMigrationPrechecks() : null; if (prechecks == null || prechecks.isEmpty()) return MigrationPrecheckResult.allow(); @@ -374,7 +365,7 @@ private MigrationPrecheckResult runPrechecks(@NotNull PendingConfirmationMigrati return MigrationPrecheckResult.allow(kickMessage); } - private @Nullable InternalProvider resolveProvider(@Nullable String providerId) { + private @Nullable InternalProvider resolveActiveProvider(@Nullable String providerId) { if (providerId == null || providerId.isBlank()) return null; for (InternalProvider provider : providerManager.getProviders()) { @@ -388,6 +379,46 @@ private MigrationPrecheckResult runPrechecks(@NotNull PendingConfirmationMigrati return null; } + private boolean isConfiguredProvider(@Nullable String providerId) { + if (providerId == null || providerId.isBlank()) return false; + + Providers providers = providersProvider.get(); + if (providers == null || providers.getProviders().isEmpty()) return false; + + for (Providers.ProviderEntry entry : providers.getProviders()) { + if (entry == null || entry.getId().isBlank()) continue; + if (entry.getId().equalsIgnoreCase(providerId)) + return true; + } + return false; + } + + private boolean supportsMigrationJourney( + @NotNull String providerId, + @NotNull PendingConfirmationMigration pendingMigration + ) { + JourneyPlan plan = migrationJourneyRegistry.resolvePlan( + MigrationContext.builder() + .connectionUniqueId(pendingMigration.connectionUniqueId()) + .identity(new ConnectionIdentity( + resolveIdenticaUniqueId(pendingMigration.uniqueId(), pendingMigration.connectionUniqueId()), + nonNull(pendingMigration.username()), + pendingMigration.ip() + )) + .targetProviderId(providerId) + .build(), + PipelineType.MIGRATION, + engineProvider.get().getScenarios().getMigration().getJourneyMode(), + providerId + ); + + for (JourneyPlan.StageEntry stage : plan.providerStages()) { + if (!stage.steps().isEmpty()) + return true; + } + return false; + } + private void closeSession(@Nullable UUID accountUniqueId) { if (accountUniqueId == null) return; diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/provider/DefaultProviderManager.java b/identica-common/src/main/java/me/whereareiam/identica/common/provider/DefaultProviderManager.java index 5415df96f..e96cebfe8 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/provider/DefaultProviderManager.java +++ b/identica-common/src/main/java/me/whereareiam/identica/common/provider/DefaultProviderManager.java @@ -11,16 +11,13 @@ import me.whereareiam.identica.model.provider.ProviderDescriptor; import me.whereareiam.identica.provider.ProviderManager; import me.whereareiam.identica.provider.resolver.ProviderResolver; -import me.whereareiam.identica.type.provider.ProviderCapability; +import me.whereareiam.identica.type.provider.ProviderFeature; import me.whereareiam.identica.type.provider.ProviderState; +import me.whereareiam.identica.type.provider.capability.ProviderCapability; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; +import java.util.*; import java.util.stream.Collectors; @Singleton @@ -112,6 +109,36 @@ public InternalProvider findProvider(ProviderCapability... capabilities) { return matches.getFirst(); } + @Override + public @NotNull List findProvidersByFeatures(@NotNull ProviderFeature... features) { + if (providers.isEmpty()) return List.of(); + + List matches = new ArrayList<>(); + for (InternalProvider provider : providers) { + if (provider == null || provider.getState() != ProviderState.ENABLED) continue; + + ProviderDescriptor descriptor = provider.getDescriptor(); + if (descriptor == null || descriptor.getId().isBlank()) continue; + if (!supportsAll(descriptor, features)) continue; + + matches.add(provider); + } + + matches.sort(Comparator.comparingInt(InternalProvider::getPriority) + .reversed() + .thenComparing(left -> left.getDescriptor().getId(), String.CASE_INSENSITIVE_ORDER)); + + return Collections.unmodifiableList(matches); + } + + @Override + public @Nullable InternalProvider findProviderByFeatures(@NotNull ProviderFeature... features) { + List matches = findProvidersByFeatures(features); + if (matches.isEmpty()) return null; + + return matches.getFirst(); + } + @Override public void registerResolver(ProviderResolver resolver) { resolverRegistry.register(resolver); @@ -168,4 +195,15 @@ private boolean supportsAll(ProviderDescriptor descriptor, ProviderCapability[] return true; } + + private boolean supportsAll(ProviderDescriptor descriptor, ProviderFeature[] features) { + if (features == null) return true; + + for (ProviderFeature feature : features) { + if (feature == null) continue; + if (!descriptor.hasFeature(feature)) return false; + } + + return true; + } } diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/provider/DefaultProviderOperations.java b/identica-common/src/main/java/me/whereareiam/identica/common/provider/DefaultProviderOperations.java index 4aec53678..2276ca898 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/provider/DefaultProviderOperations.java +++ b/identica-common/src/main/java/me/whereareiam/identica/common/provider/DefaultProviderOperations.java @@ -25,10 +25,8 @@ import me.whereareiam.identica.provider.profile.ProfileSubjectResolver; import me.whereareiam.identica.type.pipeline.PipelineType; import me.whereareiam.identica.type.pipeline.journey.JourneyMode; -import me.whereareiam.identica.type.provider.ProviderCapability; import me.whereareiam.identica.type.provider.ProviderState; import me.whereareiam.identica.util.NetworkUtil; -import me.whereareiam.identica.util.UniqueIdGenerator; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -51,8 +49,7 @@ public class DefaultProviderOperations implements ProviderOperations { @Override public @Nullable ProfileResolution resolveProfile(@NotNull ProfileResolveContext context) { List sorted = sortedEnabledProviders(); - if (sorted.isEmpty()) - return resolveOfflineProfileFallback(context); + if (sorted.isEmpty()) return null; for (InternalProvider provider : sorted) { Set registered = provider.getProfileSubjectResolvers(); @@ -75,7 +72,7 @@ public class DefaultProviderOperations implements ProviderOperations { } } - return resolveOfflineProfileFallback(context); + return null; } @Override @@ -272,22 +269,6 @@ private boolean resolversAllow( .toList(); } - private @Nullable ProfileResolution resolveOfflineProfileFallback(@NotNull ProfileResolveContext context) { - InternalProvider provider = providerManager.findProvider(ProviderCapability.OFFLINE_MODE); - if (provider == null || provider.getDescriptor() == null) return null; - - String username = context.getUsername(); - if (username == null || username.isBlank()) return null; - - UUID offlineUuid = UniqueIdGenerator.offlinePlayerUniqueId(username); - if (offlineUuid == null) return null; - - return ProfileResolution.builder() - .providerId(provider.getDescriptor().getId()) - .providerSubject(offlineUuid.toString()) - .build(); - } - private boolean isBlank(@Nullable String value) { return value == null || value.isBlank(); } diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/provider/ProviderConfiguration.java b/identica-common/src/main/java/me/whereareiam/identica/common/provider/ProviderConfiguration.java index b6720e61a..543afb225 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/provider/ProviderConfiguration.java +++ b/identica-common/src/main/java/me/whereareiam/identica/common/provider/ProviderConfiguration.java @@ -1,21 +1,22 @@ package me.whereareiam.identica.common.provider; import com.google.inject.AbstractModule; +import me.whereareiam.identica.common.provider.capability.DefaultProviderCapabilityCoordinator; +import me.whereareiam.identica.common.provider.capability.DefaultProviderCapabilityRegistry; import me.whereareiam.identica.common.provider.reader.DefaultProviderDescriptorReader; -import me.whereareiam.identica.common.provider.restriction.DefaultProviderJoinRestrictionService; -import me.whereareiam.identica.common.provider.restriction.ProviderJoinRestrictionToggleStore; import me.whereareiam.identica.provider.ProviderDescriptorReader; import me.whereareiam.identica.provider.ProviderManager; import me.whereareiam.identica.provider.ProviderOperations; -import me.whereareiam.identica.provider.restriction.ProviderJoinRestrictionService; +import me.whereareiam.identica.provider.capability.ProviderCapabilityCoordinator; +import me.whereareiam.identica.provider.capability.ProviderCapabilityRegistry; public class ProviderConfiguration extends AbstractModule { @Override protected void configure() { bind(ProviderDescriptorReader.class).to(DefaultProviderDescriptorReader.class).asEagerSingleton(); + bind(ProviderCapabilityCoordinator.class).to(DefaultProviderCapabilityCoordinator.class).asEagerSingleton(); + bind(ProviderCapabilityRegistry.class).to(DefaultProviderCapabilityRegistry.class).asEagerSingleton(); bind(ProviderManager.class).to(DefaultProviderManager.class).asEagerSingleton(); - bind(ProviderJoinRestrictionToggleStore.class).asEagerSingleton(); - bind(ProviderJoinRestrictionService.class).to(DefaultProviderJoinRestrictionService.class).asEagerSingleton(); bind(ProviderOperations.class).to(DefaultProviderOperations.class).asEagerSingleton(); } } diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/provider/ProviderLifecycleController.java b/identica-common/src/main/java/me/whereareiam/identica/common/provider/ProviderLifecycleController.java index 15d7b4405..b90356901 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/provider/ProviderLifecycleController.java +++ b/identica-common/src/main/java/me/whereareiam/identica/common/provider/ProviderLifecycleController.java @@ -1,15 +1,14 @@ package me.whereareiam.identica.common.provider; -import com.google.inject.Inject; -import com.google.inject.Injector; -import com.google.inject.Singleton; -import com.google.inject.TypeLiteral; +import com.google.inject.*; +import com.google.inject.Module; import lombok.RequiredArgsConstructor; import me.whereareiam.identica.common.config.ConfigInitializer; -import me.whereareiam.identica.common.provider.dependency.ProviderDependencyResolver; -import me.whereareiam.identica.common.provider.factory.ProviderClassLoaderFactory; +import me.whereareiam.identica.common.provider.classloader.ProviderRuntimeClassLoaderFactory; import me.whereareiam.identica.common.provider.factory.ProviderInstanceFactory; import me.whereareiam.identica.common.provider.injector.ProviderInjectorFactory; +import me.whereareiam.identica.common.provider.library.ProviderLibraryInstaller; +import me.whereareiam.identica.common.provider.library.ProviderLibraryPlanner; import me.whereareiam.identica.common.provider.resolver.ProviderPlatformExtensionResolver; import me.whereareiam.identica.common.provider.resolver.ProviderResolverRegistry; import me.whereareiam.identica.common.provider.resolver.ProviderWorkingPathResolver; @@ -26,18 +25,23 @@ import me.whereareiam.identica.logging.Logger; import me.whereareiam.identica.model.provider.InternalProvider; import me.whereareiam.identica.model.provider.ProviderDescriptor; +import me.whereareiam.identica.model.provider.dependency.ProviderLibraries; import me.whereareiam.identica.provider.IdenticaProvider; import me.whereareiam.identica.provider.ProviderPlatformBinding; import me.whereareiam.identica.provider.ProviderPlatformExtension; +import me.whereareiam.identica.provider.capability.ProviderCapabilityCoordinator; +import me.whereareiam.identica.provider.capability.bootstrap.ProviderCapabilityBootstrap; import me.whereareiam.identica.provider.eligibility.ProviderEligibilityResolver; import me.whereareiam.identica.provider.migration.ProviderMigrationPrecheck; import me.whereareiam.identica.provider.profile.ProfileSubjectResolver; import me.whereareiam.identica.provider.resolver.ProviderResolver; +import me.whereareiam.identica.type.provider.ProviderFeature; import me.whereareiam.identica.type.provider.ProviderState; import java.net.URLClassLoader; import java.nio.file.Path; import java.util.List; +import java.util.Locale; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -52,12 +56,14 @@ public class ProviderLifecycleController { private static final TypeLiteral> PLATFORM_BINDINGS = new TypeLiteral<>() {}; private final ProviderWorkingPathResolver workingPathResolver; - private final ProviderClassLoaderFactory classLoaderFactory; - private final ProviderDependencyResolver dependencyResolver; + private final ProviderRuntimeClassLoaderFactory providerRuntimeClassLoaderFactory; + private final ProviderLibraryPlanner providerLibraryPlanner; + private final ProviderLibraryInstaller providerLibraryInstaller; private final ProviderInjectorFactory injectorFactory; private final ProviderInstanceFactory instanceFactory; private final ProviderPlatformExtensionResolver platformExtensionResolver; private final ProviderResolverRegistry resolverRegistry; + private final ProviderCapabilityCoordinator capabilityCoordinator; private final ConflictService conflictService; private final SchemaBootstrap schemaBootstrap; private final EventManager eventManager; @@ -70,18 +76,17 @@ public void loadProvider(InternalProvider internal) { if (internal == null || internal.getState() != ProviderState.DISCOVERED) return; Logger.debug("Loading provider %s", safeId(internal)); + URLClassLoader classLoader = null; try { ProviderDescriptor descriptor = internal.getDescriptor(); Path workingPath = workingPathResolver.resolve(descriptor); - URLClassLoader classLoader = classLoaderFactory.create(internal.getPath()); - - dependencyResolver.loadDescriptorLibraries(descriptor, classLoader); + classLoader = providerRuntimeClassLoaderFactory.create(internal.getPath()); Class providerClass = classLoader.loadClass(descriptor.getMain()); if (!IdenticaProvider.class.isAssignableFrom(providerClass)) { Logger.warn("Provider main class does not extend IdenticaProvider: %s", descriptor.getId()); internal.setState(ProviderState.FAILED); - classLoaderFactory.close(classLoader); + providerRuntimeClassLoaderFactory.close(classLoader); return; } @@ -92,18 +97,40 @@ public void loadProvider(InternalProvider internal) { probeProvider.setWorkingPath(workingPath); } + ProviderLibraries libraries = probeProvider != null + ? probeProvider.libraries() + : ProviderLibraries.empty(); + + ProviderLibraryPlanner.ProviderLibraryPlan libraryPlan = providerLibraryPlanner.plan(libraries); + providerLibraryInstaller.installSharedCapabilityApis(libraryPlan.sharedCapabilityApis()); + providerLibraryInstaller.installProviderRuntime(descriptor, libraryPlan.providerRuntimeLibraries(), classLoader); Class platformExtensionClass = platformExtensionResolver.resolve(probeProvider); ProviderPlatformExtension probePlatformExtension = platformExtensionClass != null ? instanceFactory.instantiatePlatformExtension(platformExtensionClass) : null; - dependencyResolver.loadProviderLibraries(descriptor, probeProvider, classLoader); + List capabilityBootstraps = capabilityCoordinator.resolveBootstraps( + descriptor, + probeProvider != null ? probeProvider.declaredCapabilities() : List.of() + ); + descriptor.setDeclaredFeatureIds(probeProvider != null + ? probeProvider.declaredFeatures().stream() + .filter(feature -> feature != null && !feature.getId().isBlank()) + .map(ProviderFeature::getId) + .map(id -> id.trim().toLowerCase(Locale.ROOT)) + .distinct() + .toList() + : List.of()); + internal.setWorkingPath(workingPath); + capabilityCoordinator.installGlobalCapabilities(internal, capabilityBootstraps); + List capabilityModules = capabilityCoordinator.resolveLocalModules(internal, capabilityBootstraps); Injector providerInjector = injectorFactory.create( workingPath, descriptor, probeProvider, - probePlatformExtension + probePlatformExtension, + capabilityModules ); applySchemaContributors(providerInjector); @@ -114,7 +141,7 @@ public void loadProvider(InternalProvider internal) { ); if (provider == null) { internal.setState(ProviderState.FAILED); - classLoaderFactory.close(classLoader); + providerRuntimeClassLoaderFactory.close(classLoader); return; } @@ -134,6 +161,11 @@ public void loadProvider(InternalProvider internal) { internal.setClassLoader(classLoader); prewarmProviderConfigs(providerInjector, internal); storeBindings(internal, providerInjector); + capabilityCoordinator.validateCapabilityContributions( + descriptor, + capabilityBootstraps, + internal.getCapabilityContributions() != null ? internal.getCapabilityContributions() : Set.of() + ); internal.setState(ProviderState.LOADED); if (checkRequirements(internal)) return; @@ -141,6 +173,9 @@ public void loadProvider(InternalProvider internal) { provider.onLoad(); fireProviderLoaded(internal); } catch (Exception e) { + if (classLoader != null && internal.getClassLoader() != classLoader) + providerRuntimeClassLoaderFactory.close(classLoader); + internal.setState(ProviderState.FAILED); Logger.warn("Failed to load provider %s: %s", safeId(internal), e.getMessage()); } @@ -196,7 +231,7 @@ public void unloadProvider(InternalProvider internal) { Logger.warn("Failed to unload provider %s: %s", safeId(internal), e.getMessage()); fireProviderUnloaded(internal); } finally { - classLoaderFactory.close(internal.getClassLoader()); + providerRuntimeClassLoaderFactory.close(internal.getClassLoader()); } } @@ -237,6 +272,7 @@ private void storeBindings(InternalProvider internal, Injector injector) { internal.setEligibilityResolvers(copySet(resolveSet(injector, ELIGIBILITY_RESOLVERS))); internal.setProfileSubjectResolvers(copySet(resolveSet(injector, PROFILE_RESOLVERS))); internal.setMigrationPrechecks(copySet(resolveSet(injector, MIGRATION_PRECHECKS))); + internal.setCapabilityContributions(copySet(capabilityCoordinator.resolveCapabilityContributions(injector))); } private void registerProviderBindings(InternalProvider internal) { diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/provider/capability/DefaultProviderCapabilityCoordinator.java b/identica-common/src/main/java/me/whereareiam/identica/common/provider/capability/DefaultProviderCapabilityCoordinator.java new file mode 100644 index 000000000..07bbd38cc --- /dev/null +++ b/identica-common/src/main/java/me/whereareiam/identica/common/provider/capability/DefaultProviderCapabilityCoordinator.java @@ -0,0 +1,279 @@ +package me.whereareiam.identica.common.provider.capability; + +import com.google.inject.*; +import com.google.inject.Module; +import com.google.inject.name.Names; +import lombok.RequiredArgsConstructor; +import me.whereareiam.identica.common.provider.classloader.SharedCapabilityClassLoaderFactory; +import me.whereareiam.identica.logging.Logger; +import me.whereareiam.identica.model.provider.InternalProvider; +import me.whereareiam.identica.model.provider.ProviderDescriptor; +import me.whereareiam.identica.model.provider.capability.ProviderCapabilityDeclaration; +import me.whereareiam.identica.model.provider.capability.ProviderCapabilityInstallation; +import me.whereareiam.identica.provider.capability.ProviderCapabilityCoordinator; +import me.whereareiam.identica.provider.capability.ProviderCapabilityRegistry; +import me.whereareiam.identica.provider.capability.bootstrap.ProviderCapabilityBootstrap; +import me.whereareiam.identica.provider.capability.bootstrap.ProviderCapabilityGlobalInstallContext; +import me.whereareiam.identica.provider.capability.bootstrap.ProviderCapabilityInitializationContext; +import me.whereareiam.identica.provider.capability.bootstrap.ProviderCapabilityLocalInstallContext; +import me.whereareiam.identica.provider.capability.contribution.ProviderCapabilityContribution; +import me.whereareiam.identica.type.provider.capability.ProviderCapability; +import me.whereareiam.identica.type.provider.capability.ProviderCapabilityScope; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.nio.file.Path; +import java.util.*; + +@Singleton +@RequiredArgsConstructor(onConstructor_ = @Inject) +public class DefaultProviderCapabilityCoordinator implements ProviderCapabilityCoordinator { + private static final TypeLiteral> CAPABILITY_CONTRIBUTIONS = new TypeLiteral<>() {}; + + private final Injector rootInjector; + private final ProviderCapabilityRegistry capabilityRegistry; + private final SharedCapabilityClassLoaderFactory sharedCapabilityClassLoaderFactory; + + @Override + public @NotNull List resolveBootstraps( + @NotNull ProviderDescriptor descriptor, + @Nullable List bootstraps + ) { + List resolvedBootstraps = new ArrayList<>(); + List resolvedDeclaredCapabilityIds = new ArrayList<>(); + for (ProviderCapabilityBootstrap bootstrap : bootstraps != null ? bootstraps : List.of()) { + if (bootstrap == null) continue; + + ProviderCapabilityDeclaration declaration = bootstrap.declaration(); + String capabilityId = declaration.getCapability().getId(); + if (resolvedDeclaredCapabilityIds.contains(capabilityId)) + throw new IllegalStateException("Provider declared duplicate capability bootstrap: " + capabilityId); + + resolvedBootstraps.add(bootstrap); + resolvedDeclaredCapabilityIds.add(capabilityId); + } + + descriptor.setDeclaredCapabilityIds(List.copyOf(resolvedDeclaredCapabilityIds)); + return List.copyOf(resolvedBootstraps); + } + + @Override + public void installGlobalCapabilities( + @NotNull InternalProvider provider, + @NotNull List bootstraps + ) { + for (ProviderCapabilityBootstrap bootstrap : bootstraps) { + if (bootstrap.declaration().getScopes().contains(ProviderCapabilityScope.GLOBAL)) + installGlobalCapability(provider, bootstrap); + } + } + + @Override + public @NotNull List resolveLocalModules( + @NotNull InternalProvider provider, + @NotNull List bootstraps + ) { + ProviderDescriptor descriptor = provider.getDescriptor(); + if (descriptor == null || provider.getWorkingPath() == null) return List.of(); + + List modules = new ArrayList<>(); + List globalInjectors = new ArrayList<>(); + for (ProviderCapabilityBootstrap bootstrap : bootstraps) { + ProviderCapabilityDeclaration capabilityDeclaration = bootstrap.declaration(); + ProviderCapabilityInstallation installation = capabilityRegistry.findInstallation(capabilityDeclaration.getCapability()); + + if (installation != null && installation.getGlobalInjector() != null) + globalInjectors.add(installation.getGlobalInjector()); + + if (!capabilityDeclaration.getScopes().contains(ProviderCapabilityScope.LOCAL)) continue; + modules.addAll(resolveLocalModules(bootstrap, capabilityDeclaration.getCapability(), descriptor.getId(), provider.getWorkingPath())); + } + if (!globalInjectors.isEmpty()) + modules.addFirst(new CapabilityGlobalBridgeModule(rootInjector, sharedCapabilityClassLoaderFactory.sharedClassLoader(), globalInjectors)); + + return List.copyOf(modules); + } + + @Override + public @NotNull Set resolveCapabilityContributions(@NotNull Injector injector) { + try { + Set resolved = injector.getInstance(Key.get(CAPABILITY_CONTRIBUTIONS)); + if (resolved == null || resolved.isEmpty()) return Set.of(); + + return Set.copyOf(resolved); + } catch (ConfigurationException ignored) { + return Set.of(); + } + } + + @Override + public void validateCapabilityContributions( + @NotNull ProviderDescriptor descriptor, + @NotNull List bootstraps, + @NotNull Set contributions + ) { + Set declaredCapabilityIds = new LinkedHashSet<>(); + for (ProviderCapabilityBootstrap bootstrap : bootstraps) + declaredCapabilityIds.add(bootstrap.declaration().getCapability().getId()); + + Map contributionCounts = countContributions(declaredCapabilityIds, contributions); + + for (ProviderCapabilityBootstrap bootstrap : bootstraps) { + ProviderCapabilityDeclaration capabilityDeclaration = bootstrap.declaration(); + if (!capabilityDeclaration.isRequiresContribution()) continue; + + String capabilityId = capabilityDeclaration.getCapability().getId(); + if (contributionCounts.getOrDefault(capabilityId, 0) <= 0) + throw new IllegalStateException("Provider is missing required capability contribution: " + capabilityId); + } + } + + private @NotNull Map countContributions( + @NotNull Set declaredCapabilityIds, + @NotNull Set contributions + ) { + Map contributionCounts = new LinkedHashMap<>(); + for (ProviderCapabilityContribution contribution : contributions) { + if (contribution == null) continue; + + String capabilityId = contribution.capability().getId(); + if (!declaredCapabilityIds.contains(capabilityId)) + throw new IllegalStateException("Provider exposed undeclared capability contribution: " + capabilityId); + + contributionCounts.merge(capabilityId, 1, Integer::sum); + } + + return contributionCounts; + } + + private @NotNull List resolveLocalModules( + @NotNull ProviderCapabilityBootstrap bootstrap, + @NotNull ProviderCapability capability, + @NotNull String providerId, + @NotNull Path workingPath + ) { + return bootstrap.localModules(ProviderCapabilityLocalInstallContext.builder() + .capability(capability) + .providerId(providerId) + .workingPath(workingPath) + .build()); + } + + private void installGlobalCapability(@NotNull InternalProvider provider, @NotNull ProviderCapabilityBootstrap bootstrap) { + ProviderCapability capability = bootstrap.declaration().getCapability(); + ProviderCapabilityInstallation installation = capabilityRegistry.findInstallation(capability); + if (installation != null) { + validateCompatibleBootstrap(capability, installation.getBootstrap(), bootstrap); + return; + } + + List modules = bootstrap.globalModules(ProviderCapabilityGlobalInstallContext.builder() + .capability(capability) + .rootInjector(rootInjector) + .capabilitiesPath(rootInjector.getInstance(Key.get(Path.class, Names.named("capabilitiesPath")))) + .build()); + List injectorModules = new ArrayList<>(resolveGlobalBridgeModules()); + injectorModules.addAll(modules); + Injector capabilityInjector = injectorModules.isEmpty() ? null : rootInjector.createChildInjector(injectorModules); + bootstrap.initialize(ProviderCapabilityInitializationContext.builder() + .capability(capability) + .rootInjector(rootInjector) + .globalInjector(capabilityInjector) + .build()); + capabilityRegistry.registerInstallation(ProviderCapabilityInstallation.builder() + .bootstrap(bootstrap) + .globalInjector(capabilityInjector) + .build()); + + Logger.debug("Installed provider capability %s for provider %s", capability.getId(), safeId(provider)); + } + + private void validateCompatibleBootstrap( + @NotNull ProviderCapability capability, + @NotNull ProviderCapabilityBootstrap existing, + @NotNull ProviderCapabilityBootstrap requested + ) { + if (existing.getClass().getName().equals(requested.getClass().getName()) + && existing.declaration().getScopes().equals(requested.declaration().getScopes()) + && existing.declaration().isRequiresContribution() == requested.declaration().isRequiresContribution()) + return; + + throw new IllegalStateException("Capability bootstrap conflict for " + capability.getId() + + ": " + existing.getClass().getName() + + " != " + requested.getClass().getName()); + } + + private @NotNull String safeId(@NotNull InternalProvider provider) { + ProviderDescriptor descriptor = provider.getDescriptor(); + return descriptor != null ? safeId(descriptor) : "unknown"; + } + + private @NotNull String safeId(@NotNull ProviderDescriptor descriptor) { + if (descriptor.getId().isBlank()) return "unknown"; + + return descriptor.getId(); + } + private @NotNull List resolveGlobalBridgeModules() { + List globalInjectors = new ArrayList<>(); + for (ProviderCapabilityInstallation installation : capabilityRegistry.installations()) { + if (installation == null || installation.getGlobalInjector() == null) continue; + + globalInjectors.add(installation.getGlobalInjector()); + } + if (globalInjectors.isEmpty()) return List.of(); + + return List.of(new CapabilityGlobalBridgeModule(rootInjector, sharedCapabilityClassLoaderFactory.sharedClassLoader(), globalInjectors)); + } + + private static final class CapabilityGlobalBridgeModule extends AbstractModule { + private final Injector parentInjector; + private final ClassLoader sharedCapabilityApiClassLoader; + private final List globalInjectors; + private final ClassLoader parentClassLoader; + + private CapabilityGlobalBridgeModule( + @NotNull Injector parentInjector, + @NotNull ClassLoader sharedCapabilityApiClassLoader, + @NotNull List globalInjectors + ) { + this.parentInjector = parentInjector; + this.sharedCapabilityApiClassLoader = sharedCapabilityApiClassLoader; + this.globalInjectors = List.copyOf(globalInjectors); + this.parentClassLoader = parentInjector.getClass().getClassLoader(); + } + + @Override + protected void configure() { + Set> seen = new HashSet<>(); + for (Injector globalInjector : globalInjectors) { + for (Key key : globalInjector.getBindings().keySet()) { + if (!seen.add(key) || shouldSkip(key)) continue; + + bindBridge(globalInjector, key); + } + } + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private void bindBridge(@NotNull Injector globalInjector, @NotNull Key key) { + bind((Key) key).toProvider(globalInjector.getProvider(key)); + } + + private boolean shouldSkip(@NotNull Key key) { + Class rawType = key.getTypeLiteral().getRawType(); + return rawType == Injector.class + || rawType.getName().startsWith("com.google.inject.") + || isProviderLocalType(rawType) + || parentInjector.getExistingBinding(key) != null; + } + + private boolean isProviderLocalType(@NotNull Class rawType) { + ClassLoader classLoader = rawType.getClassLoader(); + if (classLoader == null) return false; + + return classLoader != parentClassLoader + && classLoader != parentClassLoader.getParent() + && classLoader != sharedCapabilityApiClassLoader; + } + } +} diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/provider/capability/DefaultProviderCapabilityRegistry.java b/identica-common/src/main/java/me/whereareiam/identica/common/provider/capability/DefaultProviderCapabilityRegistry.java new file mode 100644 index 000000000..72166a7d7 --- /dev/null +++ b/identica-common/src/main/java/me/whereareiam/identica/common/provider/capability/DefaultProviderCapabilityRegistry.java @@ -0,0 +1,38 @@ +package me.whereareiam.identica.common.provider.capability; + +import com.google.inject.Singleton; +import me.whereareiam.identica.model.provider.capability.ProviderCapabilityInstallation; +import me.whereareiam.identica.provider.capability.ProviderCapabilityRegistry; +import me.whereareiam.identica.type.provider.capability.ProviderCapability; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Singleton +public class DefaultProviderCapabilityRegistry implements ProviderCapabilityRegistry { + private final Map installations = new ConcurrentHashMap<>(); + + @Override + public @Nullable ProviderCapabilityInstallation findInstallation(@NotNull ProviderCapability capability) { + return installations.get(capability); + } + + @Override + public @NotNull ProviderCapabilityInstallation registerInstallation(@NotNull ProviderCapabilityInstallation installation) { + installations.put(installation.getBootstrap().declaration().getCapability(), installation); + return installation; + } + + @Override + public boolean unregisterInstallation(@NotNull ProviderCapability capability) { + return installations.remove(capability) != null; + } + + @Override + public @NotNull List installations() { + return List.copyOf(installations.values()); + } +} diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/provider/classloader/ProviderRuntimeClassLoaderFactory.java b/identica-common/src/main/java/me/whereareiam/identica/common/provider/classloader/ProviderRuntimeClassLoaderFactory.java new file mode 100644 index 000000000..c9b027722 --- /dev/null +++ b/identica-common/src/main/java/me/whereareiam/identica/common/provider/classloader/ProviderRuntimeClassLoaderFactory.java @@ -0,0 +1,38 @@ +package me.whereareiam.identica.common.provider.classloader; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import me.whereareiam.identica.logging.Logger; +import org.jetbrains.annotations.NotNull; + +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Path; + +@Singleton +public class ProviderRuntimeClassLoaderFactory { + private final ClassLoader parentClassLoader; + + @Inject + public ProviderRuntimeClassLoaderFactory(@NotNull SharedCapabilityClassLoaderFactory sharedCapabilityClassLoaderFactory) { + this.parentClassLoader = sharedCapabilityClassLoaderFactory.sharedClassLoader(); + } + + public URLClassLoader create(Path jarPath) throws MalformedURLException { + return new URLClassLoader( + new URL[]{jarPath.toUri().toURL()}, + parentClassLoader + ); + } + + public void close(Object classLoader) { + if (classLoader instanceof AutoCloseable closeable) { + try { + closeable.close(); + } catch (Exception e) { + Logger.warn("Failed to close provider runtime classloader: %s", e.getMessage()); + } + } + } +} diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/provider/classloader/SharedCapabilityClassLoaderFactory.java b/identica-common/src/main/java/me/whereareiam/identica/common/provider/classloader/SharedCapabilityClassLoaderFactory.java new file mode 100644 index 000000000..9445621d0 --- /dev/null +++ b/identica-common/src/main/java/me/whereareiam/identica/common/provider/classloader/SharedCapabilityClassLoaderFactory.java @@ -0,0 +1,19 @@ +package me.whereareiam.identica.common.provider.classloader; + +import com.google.inject.Singleton; +import org.jetbrains.annotations.NotNull; + +import java.net.URL; +import java.net.URLClassLoader; + +@Singleton +public class SharedCapabilityClassLoaderFactory { + private final URLClassLoader classLoader = new URLClassLoader( + new URL[0], + getClass().getClassLoader() + ); + + public @NotNull URLClassLoader sharedClassLoader() { + return classLoader; + } +} diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/provider/dependency/ProviderDependencyResolver.java b/identica-common/src/main/java/me/whereareiam/identica/common/provider/dependency/ProviderDependencyResolver.java deleted file mode 100644 index bb0635cac..000000000 --- a/identica-common/src/main/java/me/whereareiam/identica/common/provider/dependency/ProviderDependencyResolver.java +++ /dev/null @@ -1,64 +0,0 @@ -package me.whereareiam.identica.common.provider.dependency; - -import com.google.inject.Inject; -import com.google.inject.Singleton; -import com.google.inject.name.Named; -import me.whereareiam.attache.model.LibraryRequest; -import me.whereareiam.attache.platform.standalone.StandaloneLibraryManager; -import me.whereareiam.attache.type.VerbosityMode; -import me.whereareiam.identica.provider.IdenticaProvider; -import me.whereareiam.identica.model.provider.ProviderDescriptor; -import me.whereareiam.identica.model.provider.dependency.ProviderLibraries; - -import java.nio.file.Path; -import java.util.List; - -@Singleton -public class ProviderDependencyResolver { - private final Path providersPath; - private final ProviderDependencyLoggingAdapter loggingHelper; - - @Inject - public ProviderDependencyResolver( - @Named("providersPath") Path providersPath, - ProviderDependencyLoggingAdapter loggingHelper - ) { - this.providersPath = providersPath; - this.loggingHelper = loggingHelper; - } - - public void loadLibraries(String providerId, ProviderLibraries libraries, ClassLoader classLoader) { - if (libraries == null) return; - - List requests = libraries.toLibraryRequests(); - if (requests.isEmpty()) return; - - StandaloneLibraryManager libraryManager = new StandaloneLibraryManager( - loggingHelper, - providersPath, - ".libraries/" + providerId, - classLoader - ); - libraryManager.setVerbosityMode(VerbosityMode.QUIET); - libraryManager.addMavenCentral(); - libraryManager.addRepository("https://maven.whereareiam.me/release"); - libraryManager.addRepository("https://maven.whereareiam.me/development"); - - if (libraries.getRepositories() != null) - for (String repo : libraries.getRepositories()) - libraryManager.addRepository(repo); - - libraryManager.loadLibraries(requests); - } - - public void loadDescriptorLibraries(ProviderDescriptor descriptor, ClassLoader classLoader) { - if (descriptor == null) return; - loadLibraries(descriptor.getId(), descriptor.getLibraries(), classLoader); - } - - public void loadProviderLibraries(ProviderDescriptor descriptor, IdenticaProvider provider, ClassLoader classLoader) { - if (descriptor == null || provider == null) return; - ProviderLibraries libraries = provider.libraries(); - loadLibraries(descriptor.getId(), libraries, classLoader); - } -} diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/provider/factory/ProviderClassLoaderFactory.java b/identica-common/src/main/java/me/whereareiam/identica/common/provider/factory/ProviderClassLoaderFactory.java deleted file mode 100644 index 1a9069d9c..000000000 --- a/identica-common/src/main/java/me/whereareiam/identica/common/provider/factory/ProviderClassLoaderFactory.java +++ /dev/null @@ -1,29 +0,0 @@ -package me.whereareiam.identica.common.provider.factory; - -import com.google.inject.Singleton; -import me.whereareiam.identica.logging.Logger; - -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLClassLoader; -import java.nio.file.Path; - -@Singleton -public class ProviderClassLoaderFactory { - public URLClassLoader create(Path jarPath) throws MalformedURLException { - return new URLClassLoader( - new URL[]{jarPath.toUri().toURL()}, - getClass().getClassLoader() - ); - } - - public void close(Object classLoader) { - if (classLoader instanceof AutoCloseable closeable) { - try { - closeable.close(); - } catch (Exception e) { - Logger.warn("Failed to close provider classloader: %s", e.getMessage()); - } - } - } -} diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/provider/injector/ProviderInjectorFactory.java b/identica-common/src/main/java/me/whereareiam/identica/common/provider/injector/ProviderInjectorFactory.java index 170c2ecd9..741a41fd7 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/provider/injector/ProviderInjectorFactory.java +++ b/identica-common/src/main/java/me/whereareiam/identica/common/provider/injector/ProviderInjectorFactory.java @@ -8,6 +8,7 @@ import me.whereareiam.identica.model.provider.ProviderDescriptor; import me.whereareiam.identica.provider.IdenticaProvider; import me.whereareiam.identica.provider.ProviderPlatformExtension; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.nio.file.Path; @@ -23,7 +24,8 @@ public Injector create( Path workingPath, ProviderDescriptor descriptor, IdenticaProvider probeProvider, - @Nullable ProviderPlatformExtension probePlatformExtension + @Nullable ProviderPlatformExtension probePlatformExtension, + @NotNull List capabilityModules ) { List modules = new ArrayList<>(); modules.add(new ProviderInjectorConfiguration(workingPath, descriptor)); @@ -34,6 +36,7 @@ public Injector create( List platformModules = probePlatformExtension != null ? probePlatformExtension.modules() : List.of(); if (!platformModules.isEmpty()) modules.addAll(platformModules); + if (!capabilityModules.isEmpty()) modules.addAll(capabilityModules); return injector.createChildInjector(modules); } diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/provider/library/ProviderLibraryInstaller.java b/identica-common/src/main/java/me/whereareiam/identica/common/provider/library/ProviderLibraryInstaller.java new file mode 100644 index 000000000..6f7677722 --- /dev/null +++ b/identica-common/src/main/java/me/whereareiam/identica/common/provider/library/ProviderLibraryInstaller.java @@ -0,0 +1,90 @@ +package me.whereareiam.identica.common.provider.library; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.google.inject.name.Named; +import me.whereareiam.attache.platform.standalone.StandaloneLibraryManager; +import me.whereareiam.attache.type.VerbosityMode; +import me.whereareiam.identica.common.provider.classloader.SharedCapabilityClassLoaderFactory; +import me.whereareiam.identica.common.provider.dependency.ProviderDependencyLoggingAdapter; +import me.whereareiam.identica.model.provider.ProviderDescriptor; +import me.whereareiam.identica.model.provider.dependency.ProviderLibraries; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.net.URLClassLoader; +import java.nio.file.Path; + +@Singleton +public class ProviderLibraryInstaller { + private final Path providersPath; + private final Path capabilitiesPath; + private final ProviderDependencyLoggingAdapter loggingHelper; + private final SharedCapabilityClassLoaderFactory sharedCapabilityClassLoaderFactory; + + @Inject + public ProviderLibraryInstaller( + @Named("providersPath") @NotNull Path providersPath, + @Named("capabilitiesPath") @NotNull Path capabilitiesPath, + @NotNull ProviderDependencyLoggingAdapter loggingHelper, + @NotNull SharedCapabilityClassLoaderFactory sharedCapabilityClassLoaderFactory + ) { + this.providersPath = providersPath; + this.capabilitiesPath = capabilitiesPath; + this.loggingHelper = loggingHelper; + this.sharedCapabilityClassLoaderFactory = sharedCapabilityClassLoaderFactory; + } + + public void installSharedCapabilityApis(@NotNull ProviderLibraries sharedCapabilityApis) { + install( + capabilitiesPath, + ".libraries/shared", + sharedCapabilityApis, + sharedCapabilityClassLoaderFactory.sharedClassLoader() + ); + } + + public void installProviderRuntime( + @NotNull ProviderDescriptor descriptor, + @NotNull ProviderLibraries providerRuntimeLibraries, + @NotNull URLClassLoader classLoader + ) { + install( + providersPath, + ".libraries/" + descriptor.getId(), + providerRuntimeLibraries, + classLoader + ); + } + + protected void install( + @NotNull Path basePath, + @NotNull String cacheNamespace, + @Nullable ProviderLibraries libraries, + @NotNull URLClassLoader classLoader + ) { + if (libraries == null) return; + + var requests = libraries.toLibraryRequests(); + if (requests.isEmpty()) return; + + StandaloneLibraryManager libraryManager = new StandaloneLibraryManager( + loggingHelper, + basePath, + cacheNamespace, + classLoader + ); + libraryManager.setVerbosityMode(VerbosityMode.QUIET); + libraryManager.addMavenLocal(); + libraryManager.addMavenCentral(); + libraryManager.addRepository("https://maven.whereareiam.me/release"); + libraryManager.addRepository("https://maven.whereareiam.me/development"); + + if (libraries.getRepositories() != null) { + for (String repository : libraries.getRepositories()) + libraryManager.addRepository(repository); + } + + libraryManager.loadLibraries(requests); + } +} diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/provider/library/ProviderLibraryPlanner.java b/identica-common/src/main/java/me/whereareiam/identica/common/provider/library/ProviderLibraryPlanner.java new file mode 100644 index 000000000..aa6297c21 --- /dev/null +++ b/identica-common/src/main/java/me/whereareiam/identica/common/provider/library/ProviderLibraryPlanner.java @@ -0,0 +1,65 @@ +package me.whereareiam.identica.common.provider.library; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import lombok.RequiredArgsConstructor; +import me.whereareiam.identica.model.provider.dependency.ProviderLibraries; +import me.whereareiam.identica.model.provider.dependency.ProviderLibrary; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +@Singleton +@RequiredArgsConstructor(onConstructor_ = @Inject) +public class ProviderLibraryPlanner { + private final SharedLibraryConflictTracker sharedConflicts; + + public @NotNull ProviderLibraryPlan plan(@Nullable ProviderLibraries libraries) { + if (libraries == null) return ProviderLibraryPlan.empty(); + + List sharedCapabilityApis = new ArrayList<>(); + List providerRuntimeLibraries = new ArrayList<>(); + for (ProviderLibrary library : libraries.getLibraries() != null ? libraries.getLibraries() : List.of()) { + if (library == null) continue; + + if (isSharedCapabilityApi(library)) { + if (sharedConflicts.register(library)) + sharedCapabilityApis.add(library); + continue; + } + + providerRuntimeLibraries.add(library); + } + + return new ProviderLibraryPlan( + copyOf(libraries, sharedCapabilityApis), + copyOf(libraries, providerRuntimeLibraries) + ); + } + + private @NotNull ProviderLibraries copyOf( + @NotNull ProviderLibraries source, + @NotNull List libraries + ) { + ProviderLibraries copy = new ProviderLibraries(); + copy.setRepositories(source.getRepositories() == null ? null : List.copyOf(source.getRepositories())); + copy.setLibraries(List.copyOf(libraries)); + return copy; + } + + private boolean isSharedCapabilityApi(@NotNull ProviderLibrary library) { + String loader = library.getLoader(); + return loader != null && "shared".equalsIgnoreCase(loader.trim()); + } + + public record ProviderLibraryPlan( + @NotNull ProviderLibraries sharedCapabilityApis, + @NotNull ProviderLibraries providerRuntimeLibraries + ) { + public static @NotNull ProviderLibraryPlan empty() { + return new ProviderLibraryPlan(ProviderLibraries.empty(), ProviderLibraries.empty()); + } + } +} diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/provider/library/SharedLibraryConflictTracker.java b/identica-common/src/main/java/me/whereareiam/identica/common/provider/library/SharedLibraryConflictTracker.java new file mode 100644 index 000000000..aa2ddc083 --- /dev/null +++ b/identica-common/src/main/java/me/whereareiam/identica/common/provider/library/SharedLibraryConflictTracker.java @@ -0,0 +1,50 @@ +package me.whereareiam.identica.common.provider.library; + +import com.google.inject.Singleton; +import me.whereareiam.identica.model.provider.dependency.ProviderLibrary; +import org.jetbrains.annotations.NotNull; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + +@Singleton +public class SharedLibraryConflictTracker { + private final Map definitions = new LinkedHashMap<>(); + + public synchronized boolean register(@NotNull ProviderLibrary library) { + LibraryKey key = LibraryKey.from(library); + ProviderLibrary existing = definitions.putIfAbsent(key, library); + if (existing == null) return true; + if (sameCoordinates(existing, library)) return false; + + throw new IllegalStateException("Shared capability API library conflict for " + + coordinates(existing) + + " != " + + coordinates(library)); + } + + private boolean sameCoordinates(@NotNull ProviderLibrary left, @NotNull ProviderLibrary right) { + return Objects.equals(left.getGroupId(), right.getGroupId()) + && Objects.equals(left.getArtifactId(), right.getArtifactId()) + && Objects.equals(left.getVersion(), right.getVersion()) + && Objects.equals(left.getClassifier(), right.getClassifier()); + } + + private @NotNull String coordinates(@NotNull ProviderLibrary library) { + String classifier = library.getClassifier(); + return library.getGroupId() + ":" + library.getArtifactId() + ":" + library.getVersion() + + (classifier == null || classifier.isBlank() ? "" : ":" + classifier); + } + + private record LibraryKey(String groupId, String artifactId, String classifier) { + private static @NotNull LibraryKey from(@NotNull ProviderLibrary library) { + String classifier = library.getClassifier(); + return new LibraryKey( + library.getGroupId(), + library.getArtifactId(), + classifier == null ? "" : classifier + ); + } + } +} diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/provider/restriction/DefaultProviderJoinRestrictionService.java b/identica-common/src/main/java/me/whereareiam/identica/common/provider/restriction/DefaultProviderJoinRestrictionService.java deleted file mode 100644 index d9bb668f6..000000000 --- a/identica-common/src/main/java/me/whereareiam/identica/common/provider/restriction/DefaultProviderJoinRestrictionService.java +++ /dev/null @@ -1,246 +0,0 @@ -package me.whereareiam.identica.common.provider.restriction; - -import com.google.inject.Inject; -import com.google.inject.Provider; -import com.google.inject.Singleton; -import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.database.provider.ProviderLinkPersistenceService; -import me.whereareiam.identica.identity.actor.ConnectionIdentity; -import me.whereareiam.identica.identity.session.recognition.SessionRecognitionService; -import me.whereareiam.identica.model.config.provider.Providers; -import me.whereareiam.identica.model.provider.restriction.ProviderJoinRestrictionDecision; -import me.whereareiam.identica.model.provider.restriction.ProviderJoinRestrictionStatus; -import me.whereareiam.identica.provider.restriction.ProviderJoinRestrictionService; -import me.whereareiam.identica.type.provider.ProviderJoinRestrictionCondition; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.*; -import java.util.stream.Collectors; - -@Singleton -@RequiredArgsConstructor(onConstructor_ = @Inject) -public class DefaultProviderJoinRestrictionService implements ProviderJoinRestrictionService { - private final Provider providersProvider; - private final ProviderJoinRestrictionToggleStore toggleStore; - private final SessionRecognitionService sessionRecognitionService; - private final ProviderLinkPersistenceService providerLinkPersistenceService; - - @Override - public @NotNull ProviderJoinRestrictionStatus enable(@Nullable String providerId) { - Providers.ProviderEntry provider = findProvider(providerId); - if (provider == null) return missing(normalize(providerId)); - - Providers.ProviderEntry.Restriction.Join restriction = joinRestriction(provider); - if (restriction == null || !restriction.isEnabled() || restriction.getAllow() == null) - return unconfigured(provider.getId()); - - toggleStore.enable(provider.getId()); - return resolveStatus(provider); - } - - @Override - public @NotNull ProviderJoinRestrictionStatus disable(@Nullable String providerId) { - Providers.ProviderEntry provider = findProvider(providerId); - if (provider == null) return missing(normalize(providerId)); - - toggleStore.disable(provider.getId()); - return resolveStatus(provider); - } - - @Override - public @NotNull Optional status(@Nullable String providerId) { - Providers.ProviderEntry provider = findProvider(providerId); - if (provider == null) return Optional.empty(); - - return Optional.of(resolveStatus(provider)); - } - - @Override - public @NotNull List statuses() { - List statuses = new ArrayList<>(); - for (Providers.ProviderEntry provider : providersProvider.get().getProviders()) { - if (provider == null || provider.getId().isBlank()) - continue; - - statuses.add(resolveStatus(provider)); - } - return List.copyOf(statuses); - } - - @Override - public @NotNull ProviderJoinRestrictionDecision evaluate(@Nullable String providerId) { - Providers.ProviderEntry provider = findProvider(providerId); - if (provider == null) return inactiveDecision(Set.of(), Set.of(), false); - - Providers.ProviderEntry.Restriction.Join restriction = joinRestriction(provider); - Set allow = allowSet(restriction); - if (!isActive(provider, restriction)) - return inactiveDecision(allow, Set.of(), isConfigured(restriction)); - - return ProviderJoinRestrictionDecision.builder() - .allowed(false) - .configured(isConfigured(restriction)) - .active(true) - .allow(allow) - .matchedConditions(Set.of()) - .build(); - } - - @Override - public @NotNull ProviderJoinRestrictionDecision evaluate( - @Nullable String providerId, - @Nullable String providerSubject, - @Nullable String providerUsername, - @Nullable String ip, - @Nullable ConnectionIdentity.Origin origin - ) { - Providers.ProviderEntry provider = findProvider(providerId); - if (provider == null) - return inactiveDecision(Set.of(), Set.of(), false); - - Providers.ProviderEntry.Restriction.Join restriction = joinRestriction(provider); - Set allow = allowSet(restriction); - if (!isActive(provider, restriction)) - return inactiveDecision(allow, Set.of(), isConfigured(restriction)); - if (allow.isEmpty()) - return deniedDecision(allow, Set.of()); - - Set matched = new LinkedHashSet<>(); - boolean recognized = sessionRecognitionService.matches( - provider.getId(), - providerSubject, - providerUsername, - ip, - origin - ); - if (recognized && allow.contains(ProviderJoinRestrictionCondition.RECOGNIZED)) - matched.add(ProviderJoinRestrictionCondition.RECOGNIZED); - - if ( - allow.contains(ProviderJoinRestrictionCondition.LINKED) - && providerSubject != null - && !providerSubject.isBlank() - && providerLinkPersistenceService.findBySubject(provider.getId(), providerSubject).isPresent() - ) - matched.add(ProviderJoinRestrictionCondition.LINKED); - - if (!matched.isEmpty()) - return ProviderJoinRestrictionDecision.builder() - .allowed(true) - .configured(true) - .active(true) - .allow(allow) - .matchedConditions(Collections.unmodifiableSet(matched)) - .build(); - - return deniedDecision(allow, Set.of()); - } - - private @NotNull ProviderJoinRestrictionStatus resolveStatus(@NotNull Providers.ProviderEntry provider) { - Providers.ProviderEntry.Restriction.Join restriction = joinRestriction(provider); - Set allow = allowSet(restriction); - return ProviderJoinRestrictionStatus.builder() - .providerId(provider.getId()) - .active(restriction != null - && restriction.isEnabled() - && restriction.getAllow() != null - && toggleStore.isActive(provider.getId())) - .allow(allow) - .build(); - } - - private boolean isActive( - @NotNull Providers.ProviderEntry provider, - @Nullable Providers.ProviderEntry.Restriction.Join restriction - ) { - return restriction != null - && restriction.isEnabled() - && restriction.getAllow() != null - && toggleStore.isActive(provider.getId()); - } - - private boolean isConfigured(@Nullable Providers.ProviderEntry.Restriction.Join restriction) { - return restriction != null && restriction.getAllow() != null; - } - - private @NotNull Set allowSet( - @Nullable Providers.ProviderEntry.Restriction.Join restriction - ) { - if (restriction == null || restriction.getAllow() == null) - return Set.of(); - - return restriction.getAllow().stream() - .filter(Objects::nonNull) - .sorted(Comparator.comparingInt(Enum::ordinal)) - .collect(Collectors.collectingAndThen( - Collectors.toCollection(LinkedHashSet::new), - Collections::unmodifiableSet - )); - } - - private @Nullable Providers.ProviderEntry.Restriction.Join joinRestriction(@NotNull Providers.ProviderEntry provider) { - Providers.ProviderEntry.Restriction restriction = provider.getRestriction(); - return restriction != null ? restriction.getJoin() : null; - } - - private @Nullable Providers.ProviderEntry findProvider(@Nullable String providerId) { - String normalized = normalize(providerId); - if (normalized == null) return null; - - for (Providers.ProviderEntry provider : providersProvider.get().getProviders()) { - if (provider == null || provider.getId().isBlank()) continue; - if (normalized.equals(normalize(provider.getId()))) return provider; - } - - return null; - } - - private @Nullable String normalize(@Nullable String providerId) { - if (providerId == null || providerId.isBlank()) return null; - return providerId.trim().toLowerCase(Locale.ROOT); - } - - private @NotNull ProviderJoinRestrictionStatus missing(@Nullable String providerId) { - return ProviderJoinRestrictionStatus.builder() - .providerId(providerId != null ? providerId : "") - .active(false) - .allow(Set.of()) - .build(); - } - - private @NotNull ProviderJoinRestrictionStatus unconfigured(@NotNull String providerId) { - return ProviderJoinRestrictionStatus.builder() - .providerId(providerId) - .active(false) - .allow(Set.of()) - .build(); - } - - private @NotNull ProviderJoinRestrictionDecision inactiveDecision( - @NotNull Set allow, - @NotNull Set matched, - boolean configured - ) { - return ProviderJoinRestrictionDecision.builder() - .allowed(true) - .configured(configured) - .active(false) - .allow(allow) - .matchedConditions(matched) - .build(); - } - - private @NotNull ProviderJoinRestrictionDecision deniedDecision( - @NotNull Set allow, - @NotNull Set matched - ) { - return ProviderJoinRestrictionDecision.builder() - .allowed(false) - .configured(true) - .active(true) - .allow(allow) - .matchedConditions(matched) - .build(); - } -} diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/provider/restriction/ProviderJoinRestrictionToggleStore.java b/identica-common/src/main/java/me/whereareiam/identica/common/provider/restriction/ProviderJoinRestrictionToggleStore.java deleted file mode 100644 index d94ab9999..000000000 --- a/identica-common/src/main/java/me/whereareiam/identica/common/provider/restriction/ProviderJoinRestrictionToggleStore.java +++ /dev/null @@ -1,74 +0,0 @@ -package me.whereareiam.identica.common.provider.restriction; - -import com.google.inject.Inject; -import com.google.inject.Provider; -import com.google.inject.Singleton; -import me.whereareiam.identica.model.config.Replication; -import me.whereareiam.identica.model.replication.ReplicationType; -import me.whereareiam.identica.model.config.provider.Providers; -import me.whereareiam.identica.replication.ReplicationSystem; -import me.whereareiam.identica.replication.cache.ReplicatedCache; -import me.whereareiam.identica.replication.codec.SnapshotCodec; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.Locale; - -@Singleton -public class ProviderJoinRestrictionToggleStore { - private static final String ACTIVE_VALUE = "1"; - - private final Provider providersProvider; - private final ReplicatedCache cache; - - @Inject - public ProviderJoinRestrictionToggleStore( - @NotNull ReplicationSystem replicationSystem, - @NotNull Provider replicationProvider, - @NotNull Provider providersProvider - ) { - this.providersProvider = providersProvider; - ReplicationType type = ReplicationType.identity(String.class) - .withCodec(SnapshotCodec.string()); - this.cache = replicationSystem.cache(resolveNamespace(replicationProvider)) - .replicated(type); - } - - public boolean isActive(@Nullable String providerId) { - String key = normalize(providerId); - if (key == null) return false; - - return cache.get(key).join().filter(ACTIVE_VALUE::equals).isPresent(); - } - - public void enable(@Nullable String providerId) { - String key = normalize(providerId); - if (key == null) return; - - cache.put(key, ACTIVE_VALUE, ttlMs()).join(); - } - - public void disable(@Nullable String providerId) { - String key = normalize(providerId); - if (key == null) return; - - cache.invalidate(key).join(); - } - - private @Nullable String normalize(@Nullable String providerId) { - if (providerId == null || providerId.isBlank()) return null; - return providerId.trim().toLowerCase(Locale.ROOT); - } - - private long ttlMs() { - return providersProvider.get().getBehavior().joinRestrictionToggleTtlMillis(); - } - - private static @NotNull String resolveNamespace(@NotNull Provider replicationProvider) { - String namespace = replicationProvider.get().getCache().getProviderJoinRestrictions(); - if (namespace.isBlank()) - throw new IllegalStateException("replication.cache.providerJoinRestrictions is missing"); - - return namespace; - } -} diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/verification/VerificationConfiguration.java b/identica-common/src/main/java/me/whereareiam/identica/common/verification/VerificationConfiguration.java deleted file mode 100644 index c5c6c1e98..000000000 --- a/identica-common/src/main/java/me/whereareiam/identica/common/verification/VerificationConfiguration.java +++ /dev/null @@ -1,25 +0,0 @@ -package me.whereareiam.identica.common.verification; - -import com.google.inject.AbstractModule; -import com.google.inject.multibindings.Multibinder; -import me.whereareiam.identica.common.verification.challenge.VerificationChallengeStore; -import me.whereareiam.identica.common.verification.codec.VerificationStateCodec; -import me.whereareiam.identica.common.verification.enrollment.VerificationEnrollmentStore; -import me.whereareiam.identica.common.verification.type.totp.TotpVerificationMethod; -import me.whereareiam.identica.verification.VerificationMethod; -import me.whereareiam.identica.verification.VerificationRegistry; -import me.whereareiam.identica.verification.VerificationService; - -public class VerificationConfiguration extends AbstractModule { - @Override - protected void configure() { - bind(VerificationEnrollmentStore.class).asEagerSingleton(); - bind(VerificationChallengeStore.class).asEagerSingleton(); - bind(VerificationStateCodec.class).asEagerSingleton(); - bind(VerificationService.class).to(DefaultVerificationService.class).asEagerSingleton(); - bind(VerificationRegistry.class).to(DefaultVerificationRegistry.class).asEagerSingleton(); - Multibinder.newSetBinder(binder(), VerificationMethod.class) - .addBinding() - .to(TotpVerificationMethod.class); - } -} diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/verification/type/totp/state/TotpChallengeState.java b/identica-common/src/main/java/me/whereareiam/identica/common/verification/type/totp/state/TotpChallengeState.java deleted file mode 100644 index 8dd0bfe1f..000000000 --- a/identica-common/src/main/java/me/whereareiam/identica/common/verification/type/totp/state/TotpChallengeState.java +++ /dev/null @@ -1,21 +0,0 @@ -package me.whereareiam.identica.common.verification.type.totp.state; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; -import me.whereareiam.identica.model.verification.challenge.VerificationChallengeState; - -@Getter -@Setter -@ToString -@NoArgsConstructor -@AllArgsConstructor -@Builder(toBuilder = true) -public class TotpChallengeState implements VerificationChallengeState { - private String stepId; - private int attempts; - private boolean recoveryCodeUsed; -} diff --git a/identica-common/src/test/java/me/whereareiam/identica/common/config/defaults/ConflictsDefaultsTest.java b/identica-common/src/test/java/me/whereareiam/identica/common/config/defaults/ConflictsDefaultsTest.java index e214455a2..0d2cc5524 100644 --- a/identica-common/src/test/java/me/whereareiam/identica/common/config/defaults/ConflictsDefaultsTest.java +++ b/identica-common/src/test/java/me/whereareiam/identica/common/config/defaults/ConflictsDefaultsTest.java @@ -19,8 +19,8 @@ void usernameConflictDefaultsAreGenerated() { assertNotNull(rules); assertEquals(1, rules.getDefaultRule().getResolvers().size()); assertEquals("format_display", rules.getDefaultRule().getResolvers().getFirst().getId()); - assertEquals(1, rules.getPairs().size()); - assertEquals("premium", rules.getPairs().getFirst().getProviders().getFirst()); - assertEquals("credential", rules.getPairs().getFirst().getProviders().get(1)); + assertEquals(1, rules.getCases().size()); + assertEquals("premium", rules.getCases().getFirst().getWhen().path("providers").get(0).asText()); + assertEquals("credential", rules.getCases().getFirst().getWhen().path("providers").get(1).asText()); } } diff --git a/identica-common/src/test/java/me/whereareiam/identica/common/config/defaults/MessagesDefaultsTest.java b/identica-common/src/test/java/me/whereareiam/identica/common/config/defaults/MessagesDefaultsTest.java index da4bb4b0d..8e815002c 100644 --- a/identica-common/src/test/java/me/whereareiam/identica/common/config/defaults/MessagesDefaultsTest.java +++ b/identica-common/src/test/java/me/whereareiam/identica/common/config/defaults/MessagesDefaultsTest.java @@ -6,7 +6,6 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; @DisplayName("Messages Defaults") class MessagesDefaultsTest { @@ -17,11 +16,5 @@ void providerRestrictionMessagesAreGeneratedByDefaults() { assertNotNull(messages.getEngine()); assertNotNull(messages.getEngine().getPrepare()); - assertNotNull(messages.getEngine().getPrepare().getProviderRestricted()); - assertTrue(messages.getEngine().getPrepare().getProviderRestricted().stream().anyMatch(line -> line.contains("{providerName}"))); - assertNotNull(messages.getProviders()); - assertNotNull(messages.getProviders().getProviderRestriction()); - assertNotNull(messages.getProviders().getProviderRestriction().getDenied()); - assertTrue(messages.getProviders().getProviderRestriction().getDenied().stream().anyMatch(line -> line.contains("{allow}"))); } } diff --git a/identica-common/src/test/java/me/whereareiam/identica/common/config/defaults/ProvidersDefaultsTest.java b/identica-common/src/test/java/me/whereareiam/identica/common/config/defaults/ProvidersDefaultsTest.java index 9dcb404bd..71d1a34ee 100644 --- a/identica-common/src/test/java/me/whereareiam/identica/common/config/defaults/ProvidersDefaultsTest.java +++ b/identica-common/src/test/java/me/whereareiam/identica/common/config/defaults/ProvidersDefaultsTest.java @@ -5,8 +5,6 @@ import me.whereareiam.configura.type.Format; import me.whereareiam.identica.common.config.defaults.provider.ProvidersDefaults; import me.whereareiam.identica.model.config.provider.Providers; -import me.whereareiam.identica.type.provider.ProviderJoinRestrictionCondition; -import me.whereareiam.identica.type.verification.UnavailableSelectionPolicy; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -44,41 +42,32 @@ void generatedProvidersFileKeepsOptionalSubtreesAbsentByDefault(@TempDir Path te assertTrue(generated.contains("providers:")); assertTrue(generated.contains("entrypoints:")); assertFalse(generated.contains("session:"), generated); - assertFalse(generated.contains("restriction:"), generated); - assertFalse(generated.contains("verification:"), generated); + assertFalse(generated.contains("features:"), generated); assertNull(premium.getSession()); - assertNull(premium.getRestriction()); - assertNull(premium.getVerification()); + assertNull(premium.getFeatures()); } - @DisplayName("Declared verification merges provider defaults") + @DisplayName("Declared feature subtree stays generic in shared providers config") @Test - void declaredVerificationMergesProviderDefaults(@TempDir Path tempDir) throws Exception { + void declaredFeatureSubtreeStaysGeneric(@TempDir Path tempDir) throws Exception { Path providersPath = tempDir.resolve("providers.yml"); Files.writeString(providersPath, """ providers: - id: premium - verification: - enabled: false + features: + verification: + enabled: false """); Providers providers = yaml().update(providersPath, Providers.class); - Providers.ProviderEntry.Verification verification = provider(providers, "premium").getVerification(); - - assertNotNull(verification); - assertFalse(verification.isEnabled()); - assertFalse(verification.isRequired()); - assertEquals(UnavailableSelectionPolicy.KEEP_LOCKED, verification.getUnavailableSelectionPolicy()); - assertEquals(1, verification.getMethods().size()); - assertEquals("totp", verification.getMethods().getFirst().getId()); - assertTrue(verification.getMethods().getFirst().isEnabled()); - assertEquals(100, verification.getMethods().getFirst().getPriority()); + + assertNotNull(provider(providers, "premium").getFeatures()); } - @DisplayName("Declared session does not create recognition until declared") + @DisplayName("Declared session stays minimal when no provider session fields are set") @Test - void declaredSessionDoesNotCreateRecognitionUntilDeclared(@TempDir Path tempDir) throws Exception { + void declaredSessionStaysMinimalWhenNoFieldsAreSet(@TempDir Path tempDir) throws Exception { Path providersPath = tempDir.resolve("providers.yml"); Files.writeString(providersPath, """ providers: @@ -91,70 +80,6 @@ void declaredSessionDoesNotCreateRecognitionUntilDeclared(@TempDir Path tempDir) assertNotNull(session); assertNull(session.getConcurrencyPolicy()); - assertNull(session.getRecognition()); - } - - @DisplayName("Declared recognition merges its defaults once present") - @Test - void declaredRecognitionMergesItsDefaultsOncePresent(@TempDir Path tempDir) throws Exception { - Path providersPath = tempDir.resolve("providers.yml"); - Files.writeString(providersPath, """ - providers: - - id: premium - session: - recognition: - enabled: true - """); - - Providers providers = yaml().update(providersPath, Providers.class); - Providers.ProviderEntry.Session.Recognition recognition = - provider(providers, "premium").getSession().getRecognition(); - - assertNotNull(recognition); - assertTrue(recognition.getEnabled()); - assertTrue(recognition.getSignals().isEmpty()); - assertFalse(recognition.isAllowOnUntrustedIps()); - } - - @DisplayName("Declared restriction does not create join until declared") - @Test - void declaredRestrictionDoesNotCreateJoinUntilDeclared(@TempDir Path tempDir) throws Exception { - Path providersPath = tempDir.resolve("providers.yml"); - Files.writeString(providersPath, """ - providers: - - id: premium - restriction: {} - """); - - Providers providers = yaml().update(providersPath, Providers.class); - Providers.ProviderEntry.Restriction restriction = provider(providers, "premium").getRestriction(); - - assertNotNull(restriction); - assertNull(restriction.getJoin()); - } - - @DisplayName("Declared restriction join merges provider defaults") - @Test - void declaredJoinRestrictionMergesProviderDefaults(@TempDir Path tempDir) throws Exception { - Path providersPath = tempDir.resolve("providers.yml"); - Files.writeString(providersPath, """ - providers: - - id: credential - restriction: - join: - enabled: true - """); - - Providers providers = yaml().update(providersPath, Providers.class); - Providers.ProviderEntry.Restriction restriction = provider(providers, "credential").getRestriction(); - - assertNotNull(restriction); - assertNotNull(restriction.getJoin()); - assertTrue(restriction.getJoin().isEnabled()); - assertEquals( - List.of(ProviderJoinRestrictionCondition.RECOGNIZED, ProviderJoinRestrictionCondition.LINKED), - restriction.getJoin().getAllow() - ); } private Configura yaml() { diff --git a/identica-common/src/test/java/me/whereareiam/identica/common/config/defaults/SettingsDefaultsTest.java b/identica-common/src/test/java/me/whereareiam/identica/common/config/defaults/SettingsDefaultsTest.java index 6ba3b2cb0..397c5065e 100644 --- a/identica-common/src/test/java/me/whereareiam/identica/common/config/defaults/SettingsDefaultsTest.java +++ b/identica-common/src/test/java/me/whereareiam/identica/common/config/defaults/SettingsDefaultsTest.java @@ -6,9 +6,8 @@ import me.whereareiam.identica.common.config.IdenticaModule; import me.whereareiam.identica.model.Event; import me.whereareiam.identica.model.config.Settings; -import me.whereareiam.identica.type.platform.PlatformType; import me.whereareiam.identica.type.event.EventPriority; -import me.whereareiam.identica.type.session.recognition.RecognitionSignal; +import me.whereareiam.identica.type.platform.PlatformType; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -27,14 +26,7 @@ void generatedRoutingScenariosAreEmptyByDefault() { Settings settings = new SettingsDefaults().supply(new Settings()); assertNotNull(settings.getIdentity()); - assertNotNull(settings.getSessions().getRecognition()); - assertEquals( - java.util.List.of(RecognitionSignal.USERNAME, RecognitionSignal.IP, RecognitionSignal.VIRTUAL_HOST), - settings.getSessions().getRecognition().getDefaultSignals() - ); - assertTrue(settings.getSessions().getRecognition().getEligibility().getUntrustedIps().isEnabled()); assertEquals(java.time.Duration.ofHours(12), settings.getSessions().getActiveTtl()); - assertEquals(java.time.Duration.ofHours(12), settings.getSessions().getRecognition().getValidity()); assertEquals(java.time.Duration.ofMinutes(15), settings.getIdentity().getReservationTtl()); } diff --git a/identica-common/src/test/java/me/whereareiam/identica/common/conflict/DefaultConflictServiceTest.java b/identica-common/src/test/java/me/whereareiam/identica/common/conflict/DefaultConflictServiceTest.java index 374b2f522..80d2691a6 100644 --- a/identica-common/src/test/java/me/whereareiam/identica/common/conflict/DefaultConflictServiceTest.java +++ b/identica-common/src/test/java/me/whereareiam/identica/common/conflict/DefaultConflictServiceTest.java @@ -1,28 +1,29 @@ package me.whereareiam.identica.common.conflict; import com.fasterxml.jackson.databind.JsonNode; -import me.whereareiam.identica.common.conflict.resolver.defaults.KickActiveConflictResolver; -import me.whereareiam.identica.common.conflict.resolver.defaults.KickBothConflictResolver; -import me.whereareiam.identica.common.conflict.resolver.defaults.KickJoinerConflictResolver; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; import me.whereareiam.identica.conflict.ConflictGuard; +import me.whereareiam.identica.conflict.ConflictSubject; +import me.whereareiam.identica.conflict.ConflictType; import me.whereareiam.identica.conflict.resolver.ConflictResolver; import me.whereareiam.identica.model.config.provider.Conflicts; +import me.whereareiam.identica.model.conflict.ConflictAttributeKey; import me.whereareiam.identica.model.conflict.ConflictContext; import me.whereareiam.identica.model.conflict.ConflictResolution; -import me.whereareiam.identica.model.identity.provider.AccountProviderLink; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import java.util.List; import java.util.Set; -import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; import static org.junit.jupiter.api.Assertions.*; @DisplayName("Default Conflict Service") class DefaultConflictServiceTest { + private static final ConflictAttributeKey SELECTOR = ConflictAttributeKey.string("selector"); + @DisplayName("Conflict guards run before resolvers and can short-circuit resolution") @Test void guardRunsBeforeResolver() { @@ -30,99 +31,87 @@ void guardRunsBeforeResolver() { conflicts.getRules().put("username", rules(defaultRule(entry("test")))); AtomicBoolean resolverCalled = new AtomicBoolean(false); - ConflictResolver resolver = new ConflictResolver() { - @Override - public @NotNull String getId() { - return "test"; - } - - @Override - public @NotNull ConflictResolution resolve(@NotNull ConflictContext context, @NotNull JsonNode params) { - resolverCalled.set(true); - return ConflictResolution.allow(); - } - }; - + ConflictResolver resolver = resolver("test", resolverCalled, ConflictResolution.allow()); ConflictGuard guard = context -> ConflictResolution.deny("guarded"); - DefaultConflictService service = new DefaultConflictService( - () -> conflicts, - Set.of(guard), - new KickJoinerConflictResolver(), - new KickActiveConflictResolver(), - new KickBothConflictResolver() - ); + DefaultConflictService service = new DefaultConflictService(() -> conflicts, Set.of(guard)); service.register(resolver); ConflictResolution resolution = service.resolve(context("username")); - - assertEquals(ConflictResolution.Action.DENY, resolution.getAction()); + assertEquals(ConflictResolution.Decision.DENY, resolution.getDecision()); assertFalse(resolverCalled.get()); } - @DisplayName("Falls back to the default rule after pair-specific resolvers pass") + @DisplayName("Falls back to the default rule after case-specific resolvers pass") @Test - void fallsBackToDefaultRuleWhenPairResolversPass() { + void fallsBackToDefaultRuleWhenCaseResolversPass() { Conflicts conflicts = new Conflicts(); Conflicts.ConflictRules rules = new Conflicts.ConflictRules(); rules.setDefaultRule(defaultRule(entry("allow"))); - Conflicts.ConflictRules.ConflictRule pairRule = new Conflicts.ConflictRules.ConflictRule(); - pairRule.setProviders(List.of("premium", "credential")); - pairRule.setResolvers(List.of(entry("pass"))); - rules.setPairs(List.of(pairRule)); + Conflicts.ConflictRules.ConflictRule caseRule = new Conflicts.ConflictRules.ConflictRule(); + caseRule.setWhen(JsonNodeFactory.instance.objectNode().put("selector", "preferred")); + caseRule.setResolvers(List.of(entry("pass"))); + rules.setCases(List.of(caseRule)); conflicts.getRules().put("username", rules); AtomicBoolean passCalled = new AtomicBoolean(false); AtomicBoolean allowCalled = new AtomicBoolean(false); - ConflictResolver passResolver = new ConflictResolver() { - @Override - public @NotNull String getId() { - return "pass"; - } - - @Override - public @NotNull ConflictResolution resolve(@NotNull ConflictContext context, @NotNull JsonNode params) { - passCalled.set(true); - return ConflictResolution.pass(); - } - }; - ConflictResolver allowResolver = new ConflictResolver() { - @Override - public @NotNull String getId() { - return "allow"; - } - - @Override - public @NotNull ConflictResolution resolve(@NotNull ConflictContext context, @NotNull JsonNode params) { - allowCalled.set(true); - return ConflictResolution.allow(); - } - }; + ConflictResolver passResolver = resolver("pass", passCalled, ConflictResolution.pass()); + ConflictResolver allowResolver = resolver("allow", allowCalled, ConflictResolution.allow()); - DefaultConflictService service = new DefaultConflictService( - () -> conflicts, - Set.of(), - new KickJoinerConflictResolver(), - new KickActiveConflictResolver(), - new KickBothConflictResolver() - ); + DefaultConflictService service = new DefaultConflictService(() -> conflicts, Set.of()); + service.register(new TestConflictType()); service.register(passResolver); service.register(allowResolver); - ConflictContext context = context("username") - .toBuilder() - .incomingLink(link("premium")) - .existingLink(link("credential")) - .build(); + ConflictContext context = context("username"); + context.putAttribute(SELECTOR, "preferred"); ConflictResolution resolution = service.resolve(context); - assertEquals(ConflictResolution.Action.ALLOW, resolution.getAction()); + assertEquals(ConflictResolution.Decision.ALLOW, resolution.getDecision()); assertTrue(passCalled.get()); assertTrue(allowCalled.get()); } + @DisplayName("The first matching case wins") + @Test + void firstMatchingCaseWins() { + Conflicts conflicts = new Conflicts(); + Conflicts.ConflictRules rules = new Conflicts.ConflictRules(); + rules.setDefaultRule(defaultRule(entry("default"))); + + Conflicts.ConflictRules.ConflictRule first = new Conflicts.ConflictRules.ConflictRule(); + first.setWhen(JsonNodeFactory.instance.objectNode().put("selector", "preferred")); + first.setResolvers(List.of(entry("first"))); + + Conflicts.ConflictRules.ConflictRule second = new Conflicts.ConflictRules.ConflictRule(); + second.setWhen(JsonNodeFactory.instance.objectNode().put("selector", "preferred")); + second.setResolvers(List.of(entry("second"))); + + rules.setCases(List.of(first, second)); + conflicts.getRules().put("username", rules); + + AtomicBoolean firstCalled = new AtomicBoolean(false); + AtomicBoolean secondCalled = new AtomicBoolean(false); + ConflictResolver firstResolver = resolver("first", firstCalled, ConflictResolution.allow()); + ConflictResolver secondResolver = resolver("second", secondCalled, ConflictResolution.deny("denied")); + + DefaultConflictService service = new DefaultConflictService(() -> conflicts, Set.of()); + service.register(new TestConflictType()); + service.register(firstResolver); + service.register(secondResolver); + + ConflictContext context = context("username"); + context.putAttribute(SELECTOR, "preferred"); + + ConflictResolution resolution = service.resolve(context); + assertEquals(ConflictResolution.Decision.ALLOW, resolution.getDecision()); + assertTrue(firstCalled.get()); + assertFalse(secondCalled.get()); + } + private Conflicts.ConflictRules rules(Conflicts.ConflictRules.ConflictRule defaultRule) { Conflicts.ConflictRules rules = new Conflicts.ConflictRules(); rules.setDefaultRule(defaultRule); @@ -147,16 +136,40 @@ private Conflicts.ConflictRules.ConflictRule.ResolverEntry entry(String id) { private ConflictContext context(String key) { return ConflictContext.builder() .key(key) - .candidate("Player") + .hook("prepare") .build(); } - private AccountProviderLink link(String providerId) { - return AccountProviderLink.builder() - .uniqueId(UUID.randomUUID()) - .providerId(providerId) - .providerSubject("subject-" + providerId) - .primaryLink(true) - .build(); + private ConflictResolver resolver(String id, AtomicBoolean called, ConflictResolution resolution) { + return new ConflictResolver() { + @Override + public @NotNull String getId() { + return id; + } + + @Override + public @NotNull ConflictResolution resolve(@NotNull ConflictContext context, @NotNull JsonNode params) { + called.set(true); + return resolution; + } + }; + } + + private static final class TestConflictType implements ConflictType { + @Override + public @NotNull String getKey() { + return "username"; + } + + @Override + public ConflictContext createContext(@NotNull ConflictSubject subject) { + return null; + } + + @Override + public boolean matchesRule(@NotNull ConflictContext context, @NotNull JsonNode when) { + String selector = context.getAttribute(SELECTOR); + return selector != null && selector.equalsIgnoreCase(when.path("selector").asText()); + } } } diff --git a/identica-common/src/test/java/me/whereareiam/identica/common/conflict/guard/UsernameEntrypointConflictGuardTest.java b/identica-common/src/test/java/me/whereareiam/identica/common/conflict/guard/UsernameEntrypointConflictGuardTest.java deleted file mode 100644 index a3661eac1..000000000 --- a/identica-common/src/test/java/me/whereareiam/identica/common/conflict/guard/UsernameEntrypointConflictGuardTest.java +++ /dev/null @@ -1,130 +0,0 @@ -package me.whereareiam.identica.common.conflict.guard; - -import me.whereareiam.identica.common.config.defaults.messages.MessagesDefaults; -import me.whereareiam.identica.common.provider.DefaultProviderOperations; -import me.whereareiam.identica.event.EventManager; -import me.whereareiam.identica.model.config.Messages; -import me.whereareiam.identica.model.config.provider.Providers; -import me.whereareiam.identica.model.conflict.ConflictContext; -import me.whereareiam.identica.model.conflict.ConflictResolution; -import me.whereareiam.identica.model.identity.provider.AccountProviderLink; -import me.whereareiam.identica.pipeline.journey.registry.type.AuthenticationJourneyRegistry; -import me.whereareiam.identica.pipeline.journey.registry.type.MigrationJourneyRegistry; -import me.whereareiam.identica.pipeline.journey.registry.type.RegistrationJourneyRegistry; -import me.whereareiam.identica.provider.ProviderManager; -import me.whereareiam.identica.provider.ProviderOperations; -import me.whereareiam.identica.type.provider.ProviderOrigin; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.List; -import java.util.UUID; - -import static org.junit.jupiter.api.Assertions.*; - -@ExtendWith(MockitoExtension.class) -@DisplayName("Username Entrypoint Conflict Guard") -class UsernameEntrypointConflictGuardTest { - @Mock - private ProviderManager providerManager; - @Mock - private AuthenticationJourneyRegistry authenticationJourneyRegistry; - @Mock - private RegistrationJourneyRegistry registrationJourneyRegistry; - @Mock - private MigrationJourneyRegistry migrationJourneyRegistry; - @Mock - private EventManager eventManager; - - @DisplayName("Denies the conflict when the entrypoint was auto-detected and remains ambiguous") - @Test - void deniesWhenEntrypointAmbiguous() { - Providers providers = new Providers(); - providers.setProviders(List.of( - entry("premium", "Premium Network", 100, List.of("premium.example.com")), - entry("credential", "Credential Network", 50, List.of("credential.example.com")) - )); - Messages messages = new MessagesDefaults().supply(new Messages()); - - ProviderOperations providerOperations = providerOperations(providers); - UsernameEntrypointConflictGuard guard = new UsernameEntrypointConflictGuard( - () -> messages, - providerOperations - ); - - ConflictContext context = ConflictContext.builder() - .key("username") - .candidate("Player") - .incomingLink(link("premium")) - .existingLink(link("credential")) - .build(); - context.putExtra("entrypointSource", ProviderOrigin.AUTO); - - ConflictResolution resolution = guard.guard(context); - assertNotNull(resolution); - assertEquals(ConflictResolution.Action.DENY, resolution.getAction()); - assertNotNull(resolution.getMessage()); - assertTrue(resolution.getMessage().contains("Premium Network")); - assertTrue(resolution.getMessage().contains("Credential Network")); - assertTrue(resolution.getMessage().contains("premium.example.com")); - assertTrue(resolution.getMessage().contains("credential.example.com")); - } - - @DisplayName("Allows the conflict when the user has already selected an entrypoint") - @Test - void allowsWhenEntrypointSelected() { - Providers providers = new Providers(); - providers.setProviders(List.of( - entry("premium", "Premium Network", 100, List.of("premium.example.com")), - entry("credential", "Credential Network", 50, List.of("credential.example.com")) - )); - - ProviderOperations providerOperations = providerOperations(providers); - UsernameEntrypointConflictGuard guard = new UsernameEntrypointConflictGuard( - Messages::new, - providerOperations - ); - - ConflictContext context = ConflictContext.builder() - .key("username") - .candidate("Player") - .incomingLink(link("premium")) - .existingLink(link("credential")) - .build(); - context.putExtra("entrypointSource", ProviderOrigin.ENTRYPOINT); - - assertNull(guard.guard(context)); - } - - private Providers.ProviderEntry entry(String id, String displayName, int priority, List entrypoints) { - Providers.ProviderEntry entry = new Providers.ProviderEntry(); - entry.setId(id); - entry.setDisplayName(displayName); - entry.setPriority(priority); - entry.setEntrypoints(entrypoints); - return entry; - } - - private AccountProviderLink link(String providerId) { - return AccountProviderLink.builder() - .uniqueId(UUID.randomUUID()) - .providerId(providerId) - .providerSubject("subject-" + providerId) - .primaryLink(true) - .build(); - } - - private ProviderOperations providerOperations(Providers providers) { - return new DefaultProviderOperations( - providerManager, - authenticationJourneyRegistry, - registrationJourneyRegistry, - migrationJourneyRegistry, - () -> providers, - eventManager - ); - } -} diff --git a/identica-common/src/test/java/me/whereareiam/identica/common/conflict/resolver/username/FormatUsernameConflictResolverTest.java b/identica-common/src/test/java/me/whereareiam/identica/common/conflict/resolver/username/FormatUsernameConflictResolverTest.java deleted file mode 100644 index f3b8b08f9..000000000 --- a/identica-common/src/test/java/me/whereareiam/identica/common/conflict/resolver/username/FormatUsernameConflictResolverTest.java +++ /dev/null @@ -1,112 +0,0 @@ -package me.whereareiam.identica.common.conflict.resolver.username; - -import me.whereareiam.identica.model.conflict.ConflictContext; -import me.whereareiam.identica.model.conflict.ConflictResolution; -import me.whereareiam.identica.model.identity.provider.AccountProviderLink; -import me.whereareiam.identica.provider.ProviderOperations; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import java.util.UUID; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -@DisplayName("Format-Username Conflict Resolver") -class FormatUsernameConflictResolverTest { - @DisplayName("Formats the username when a custom pattern is provided") - @Test - void formatsUsernameWhenPatternProvided() { - ProviderOperations providerOperations = mock(ProviderOperations.class); - FormatUsernameConflictResolver resolver = new FormatUsernameConflictResolver(providerOperations); - - ConflictContext context = ConflictContext.builder() - .key("username") - .candidate("Player") - .incomingLink(link("premium")) - .existingLink(link("credential")) - .build(); - - FormatUsernameConflictResolver.Config config = new FormatUsernameConflictResolver.Config(); - config.getFormat().setPattern("{username}*"); - - ConflictResolution resolution = resolver.resolve(context, config); - assertEquals(ConflictResolution.Action.ALLOW, resolution.getAction()); - assertEquals("Player*", resolution.getOverrideValue()); - } - - @DisplayName("Expands provider names and IDs inside username format patterns") - @Test - void formatsDisplayNamesAndProviderIdsWhenAvailable() { - ProviderOperations providerOperations = mock(ProviderOperations.class); - when(providerOperations.displayProviderName("premium")).thenReturn("Premium"); - when(providerOperations.displayProviderName("credential")).thenReturn("Credential"); - - FormatUsernameConflictResolver resolver = new FormatUsernameConflictResolver(providerOperations); - - ConflictContext context = ConflictContext.builder() - .key("username") - .candidate("Player") - .incomingLink(link("premium")) - .existingLink(link("credential")) - .build(); - - FormatUsernameConflictResolver.Config config = new FormatUsernameConflictResolver.Config(); - config.getFormat().setPattern("{username} [{incomingProvider}] ({incomingProviderId}->{existingProviderId})"); - - ConflictResolution resolution = resolver.resolve(context, config); - assertEquals("Player [Premium] (premium->credential)", resolution.getOverrideValue()); - } - - @DisplayName("Cuts placeholder values when max symbol count is provided") - @Test - void truncatesProviderNamePlaceholdersWhenMaxSymbolCountProvided() { - ProviderOperations providerOperations = mock(ProviderOperations.class); - when(providerOperations.displayProviderName("premium")).thenReturn("PremiumPlus"); - when(providerOperations.displayProviderName("credential")).thenReturn("Credential"); - - FormatUsernameConflictResolver resolver = new FormatUsernameConflictResolver(providerOperations); - - ConflictContext context = ConflictContext.builder() - .key("username") - .candidate("PlayerOne") - .incomingLink(link("premium")) - .existingLink(link("credential")) - .build(); - - FormatUsernameConflictResolver.Config config = new FormatUsernameConflictResolver.Config(); - config.getFormat().setPattern("{username}_{incomingProvider:7}_{incomingProviderId}"); - - ConflictResolution resolution = resolver.resolve(context, config); - assertEquals("PlayerOne_Premium_premium", resolution.getOverrideValue()); - } - - @DisplayName("Keeps random placeholder digit counts working") - @Test - void keepsRandomPlaceholderDigitCountsWorking() { - ProviderOperations providerOperations = mock(ProviderOperations.class); - FormatUsernameConflictResolver resolver = new FormatUsernameConflictResolver(providerOperations); - - ConflictContext context = ConflictContext.builder() - .key("username") - .candidate("Player") - .build(); - - FormatUsernameConflictResolver.Config config = new FormatUsernameConflictResolver.Config(); - config.getFormat().setPattern("{username}_{random:4}"); - - ConflictResolution resolution = resolver.resolve(context, config); - assertTrue(resolution.getOverrideValue().matches("Player_\\d{4}")); - } - - private AccountProviderLink link(String providerId) { - return AccountProviderLink.builder() - .uniqueId(UUID.randomUUID()) - .providerId(providerId) - .providerSubject("subject-" + providerId) - .primaryLink(true) - .build(); - } -} diff --git a/identica-common/src/test/java/me/whereareiam/identica/common/identity/DefaultIdentityServiceTest.java b/identica-common/src/test/java/me/whereareiam/identica/common/identity/DefaultIdentityServiceTest.java index 6aac13862..bd07a7d0c 100644 --- a/identica-common/src/test/java/me/whereareiam/identica/common/identity/DefaultIdentityServiceTest.java +++ b/identica-common/src/test/java/me/whereareiam/identica/common/identity/DefaultIdentityServiceTest.java @@ -53,8 +53,7 @@ void sessionOpenedUpdatesLiveAttachmentAccountUniqueId() { .providerId("premium") .originalUsername(identity.getUsername()) .effectiveUsername(identity.getUsername()) - .build(), - false + .build() )); assertEquals(accountUniqueId, identity.getAccountUniqueId()); @@ -73,8 +72,7 @@ void sessionOpenedIgnoresMissingAttachment() { .providerId("premium") .originalUsername("PlayerOne") .effectiveUsername("PlayerOne") - .build(), - false + .build() )); assertTrue(service.list().isEmpty()); diff --git a/identica-common/src/test/java/me/whereareiam/identica/common/identity/account/DefaultAccountServiceTest.java b/identica-common/src/test/java/me/whereareiam/identica/common/identity/account/DefaultAccountServiceTest.java index 0847cc42d..c8d037d8b 100644 --- a/identica-common/src/test/java/me/whereareiam/identica/common/identity/account/DefaultAccountServiceTest.java +++ b/identica-common/src/test/java/me/whereareiam/identica/common/identity/account/DefaultAccountServiceTest.java @@ -11,7 +11,6 @@ import me.whereareiam.identica.model.config.Settings; import me.whereareiam.identica.model.identity.Account; import me.whereareiam.identica.model.identity.AccountOperationRequest; -import me.whereareiam.identica.type.UsernameSource; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -105,7 +104,6 @@ private Account account() { return Account.builder() .uniqueId(UUID.randomUUID()) .username("Player") - .source(UsernameSource.PROVIDER) .build(); } diff --git a/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/DefaultSessionRecognitionStoreTest.java b/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/DefaultSessionRecognitionStoreTest.java deleted file mode 100644 index eeb822113..000000000 --- a/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/DefaultSessionRecognitionStoreTest.java +++ /dev/null @@ -1,79 +0,0 @@ -package me.whereareiam.identica.common.identity.session; - -import me.whereareiam.identica.common.identity.session.recognition.DefaultSessionRecognitionStore; -import me.whereareiam.identica.common.replication.DefaultReplicationSystem; -import me.whereareiam.identica.common.replication.ReplicationTestFixtures; -import me.whereareiam.identica.model.config.Replication; -import me.whereareiam.identica.model.config.Settings; -import me.whereareiam.identica.model.session.SessionRecognitionSnapshot; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -@DisplayName("Default Session-Recognition Store") -class DefaultSessionRecognitionStoreTest { - @DisplayName("Recognition snapshots are keyed by provider id and subject") - @Test - void snapshotsUseProviderIdAndSubjectKey() { - DefaultSessionRecognitionStore store = new DefaultSessionRecognitionStore( - this::settings, - this::replication, - new DefaultReplicationSystem(new ReplicationTestFixtures.TestReplicationAdapter()) - ); - - store.save(SessionRecognitionSnapshot.builder() - .providerId("premium") - .providerSubject("subject-1") - .providerUsername("PlayerOne") - .lastIp("127.0.0.1") - .capturedAt(System.currentTimeMillis()) - .build()); - - SessionRecognitionSnapshot found = store.find("premium", "subject-1").orElse(null); - assertNotNull(found); - assertEquals("PlayerOne", found.getProviderUsername()); - assertEquals("127.0.0.1", found.getLastIp()); - } - - @DisplayName("Recognition snapshots do not leak across subjects") - @Test - void snapshotsRequireMatchingSubject() { - DefaultSessionRecognitionStore store = new DefaultSessionRecognitionStore( - this::settings, - this::replication, - new DefaultReplicationSystem(new ReplicationTestFixtures.TestReplicationAdapter()) - ); - - store.save(SessionRecognitionSnapshot.builder() - .providerId("premium") - .providerSubject("subject-1") - .providerUsername("PlayerOne") - .capturedAt(System.currentTimeMillis()) - .build()); - - assertTrue(store.find("premium", "subject-2").isEmpty()); - } - - private Replication replication() { - Replication replication = new Replication(); - Replication.Cache cache = new Replication.Cache(); - Replication.Sessions sessions = new Replication.Sessions(); - Replication.Sessions.Recognition recognition = new Replication.Sessions.Recognition(); - recognition.setSnapshot("session-recognition:snapshot"); - sessions.setRecognition(recognition); - cache.setSessions(sessions); - replication.setCache(cache); - return replication; - } - - private Settings settings() { - Settings settings = new Settings(); - Settings.Sessions sessions = new Settings.Sessions(); - Settings.Sessions.Recognition recognition = new Settings.Sessions.Recognition(); - recognition.setValidity(java.time.Duration.ofHours(12)); - sessions.setRecognition(recognition); - settings.setSessions(sessions); - return settings; - } -} diff --git a/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/DefaultSessionServiceTest.java b/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/DefaultSessionServiceTest.java index 6f17e0c85..105c99c83 100644 --- a/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/DefaultSessionServiceTest.java +++ b/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/DefaultSessionServiceTest.java @@ -10,9 +10,9 @@ import me.whereareiam.identica.event.identity.session.SessionClosedEvent; import me.whereareiam.identica.model.Session; import me.whereareiam.identica.model.SessionCloseRequest; -import me.whereareiam.identica.model.config.provider.Providers; import me.whereareiam.identica.model.config.Replication; import me.whereareiam.identica.model.config.Settings; +import me.whereareiam.identica.model.config.provider.Providers; import me.whereareiam.identica.model.scheduler.*; import me.whereareiam.identica.service.Scheduler; import me.whereareiam.identica.type.session.SessionConcurrencyPolicy; @@ -249,9 +249,6 @@ private Settings settings() { Settings.Sessions sessions = new Settings.Sessions(); sessions.setConcurrencyPolicy(SessionConcurrencyPolicy.REPLACE_EXISTING); sessions.setActiveTtl(java.time.Duration.ofHours(12)); - Settings.Sessions.Recognition recognition = new Settings.Sessions.Recognition(); - recognition.setValidity(java.time.Duration.ofHours(12)); - sessions.setRecognition(recognition); settings.setSessions(sessions); return settings; } @@ -272,9 +269,6 @@ private Replication replication(String serverId) { sessions.setUser("sessions:user"); sessions.setSession("sessions:session"); sessions.setSubject("sessions:subject"); - Replication.Sessions.Recognition recognition = new Replication.Sessions.Recognition(); - recognition.setSnapshot("session-recognition:snapshot"); - sessions.setRecognition(recognition); cache.setSessions(sessions); replication.setCache(cache); diff --git a/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/rule/RecognitionEnabledEligibilityRuleTest.java b/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/rule/RecognitionEnabledEligibilityRuleTest.java deleted file mode 100644 index 31b2babde..000000000 --- a/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/rule/RecognitionEnabledEligibilityRuleTest.java +++ /dev/null @@ -1,77 +0,0 @@ -package me.whereareiam.identica.common.identity.session.recognition.eligibility.rule; - -import me.whereareiam.identica.model.config.provider.Providers; -import me.whereareiam.identica.model.config.Settings; -import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityContext; -import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityRuleDecision; -import me.whereareiam.identica.type.session.recognition.RecognitionAttemptKind; -import me.whereareiam.identica.type.session.recognition.RecognitionTrigger; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import java.time.Duration; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -@DisplayName("Recognition Enabled Eligibility Rule") -class RecognitionEnabledEligibilityRuleTest { - @DisplayName("Blocks session recognition when recognition is disabled") - @Test - void blocksSessionRecognitionWhenRecognitionIsDisabled() { - Settings settings = settings(false); - Providers providers = providers(null); - - RecognitionEligibilityRuleDecision decision = new RecognitionEnabledEligibilityRule(() -> settings, () -> providers) - .evaluate(context(RecognitionAttemptKind.SESSION_RECOGNITION)); - - assertEquals(RecognitionEligibilityRuleDecision.Status.BLOCK, decision.getStatus()); - } - - @DisplayName("Provider recognition settings restore recognition when enabled") - @Test - void providerRecognitionSettingsRestoreRecognitionWhenEnabled() { - Settings settings = settings(false); - Providers providers = providers(true); - - RecognitionEligibilityRuleDecision decision = new RecognitionEnabledEligibilityRule(() -> settings, () -> providers) - .evaluate(context(RecognitionAttemptKind.SESSION_RECOGNITION)); - - assertEquals(RecognitionEligibilityRuleDecision.Status.ABSTAIN, decision.getStatus()); - } - - private RecognitionEligibilityContext context(RecognitionAttemptKind attemptKind) { - return RecognitionEligibilityContext.builder() - .providerId("premium") - .attemptKind(attemptKind) - .trigger(RecognitionTrigger.AUTOMATIC) - .build(); - } - - private Settings settings(boolean enabled) { - Settings.Sessions.Recognition recognition = new Settings.Sessions.Recognition(); - recognition.setEnabled(enabled); - recognition.setValidity(Duration.ofHours(12)); - - Settings.Sessions sessions = new Settings.Sessions(); - sessions.setRecognition(recognition); - - Settings settings = new Settings(); - settings.setSessions(sessions); - return settings; - } - - private Providers providers(Boolean enabledOverride) { - Providers.ProviderEntry provider = new Providers.ProviderEntry(); - provider.setId("premium"); - Providers.ProviderEntry.Session.Recognition recognition = new Providers.ProviderEntry.Session.Recognition(); - recognition.setEnabled(enabledOverride); - Providers.ProviderEntry.Session session = new Providers.ProviderEntry.Session(); - session.setRecognition(recognition); - provider.setSession(session); - - Providers providers = new Providers(); - providers.setProviders(List.of(provider)); - return providers; - } -} diff --git a/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/rule/UntrustedIpRecognitionEligibilityRuleTest.java b/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/rule/UntrustedIpRecognitionEligibilityRuleTest.java deleted file mode 100644 index ac457c49f..000000000 --- a/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/rule/UntrustedIpRecognitionEligibilityRuleTest.java +++ /dev/null @@ -1,93 +0,0 @@ -package me.whereareiam.identica.common.identity.session.recognition.eligibility.rule; - -import me.whereareiam.identica.model.config.provider.Providers; -import me.whereareiam.identica.model.config.Settings; -import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityContext; -import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityRuleDecision; -import me.whereareiam.identica.type.session.recognition.RecognitionAttemptKind; -import me.whereareiam.identica.type.session.recognition.RecognitionTrigger; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; - -@DisplayName("Untrusted IP Recognition Eligibility Rule") -class UntrustedIpRecognitionEligibilityRuleTest { - @DisplayName("Matches exact IPv4 entries") - @Test - void matchesExactIpv4Entries() { - RecognitionEligibilityRuleDecision decision = rule(settings(true, List.of("203.0.113.10")), providers(false)) - .evaluate(context("203.0.113.10")); - - assertEquals(RecognitionEligibilityRuleDecision.Status.BLOCK, decision.getStatus()); - } - - @DisplayName("Provider recognition settings allow recognition on untrusted IPs") - @Test - void providerRecognitionSettingsAllowRecognitionOnUntrustedIps() { - RecognitionEligibilityRuleDecision decision = rule(settings(true, List.of("203.0.113.10")), providers(true)) - .evaluate(context("203.0.113.10")); - - assertEquals(RecognitionEligibilityRuleDecision.Status.ALLOW, decision.getStatus()); - } - - @DisplayName("Invalid entries fail with the eligibility config path") - @Test - void invalidEntriesFailWithEligibilityConfigPath() { - IllegalStateException exception = assertThrows( - IllegalStateException.class, - () -> rule(settings(true, List.of("not-an-ip")), providers(false)).evaluate(context("203.0.113.10")) - ); - - assertTrue(exception.getMessage().contains("settings.sessions.recognition.eligibility.untrustedIps.entries[0]")); - } - - private UntrustedIpRecognitionEligibilityRule rule(Settings settings, Providers providers) { - return new UntrustedIpRecognitionEligibilityRule(() -> settings, () -> providers); - } - - private RecognitionEligibilityContext context(String ip) { - return RecognitionEligibilityContext.builder() - .providerId("premium") - .clientIp(ip) - .attemptKind(RecognitionAttemptKind.SESSION_RECOGNITION) - .trigger(RecognitionTrigger.AUTOMATIC) - .build(); - } - - private Settings settings(boolean enabled, List entries) { - Settings.Sessions.Recognition.Eligibility.UntrustedIps untrustedIps = - new Settings.Sessions.Recognition.Eligibility.UntrustedIps(); - untrustedIps.setEnabled(enabled); - untrustedIps.setEntries(entries); - - Settings.Sessions.Recognition.Eligibility eligibility = new Settings.Sessions.Recognition.Eligibility(); - eligibility.setUntrustedIps(untrustedIps); - - Settings.Sessions.Recognition recognition = new Settings.Sessions.Recognition(); - recognition.setEligibility(eligibility); - - Settings.Sessions sessions = new Settings.Sessions(); - sessions.setRecognition(recognition); - - Settings settings = new Settings(); - settings.setSessions(sessions); - return settings; - } - - private Providers providers(boolean allowOnUntrustedIp) { - Providers.ProviderEntry premium = new Providers.ProviderEntry(); - premium.setId("premium"); - Providers.ProviderEntry.Session.Recognition recognition = new Providers.ProviderEntry.Session.Recognition(); - recognition.setAllowOnUntrustedIps(allowOnUntrustedIp); - Providers.ProviderEntry.Session session = new Providers.ProviderEntry.Session(); - session.setRecognition(recognition); - premium.setSession(session); - - Providers providers = new Providers(); - providers.setProviders(List.of(premium)); - return providers; - } -} diff --git a/identica-common/src/test/java/me/whereareiam/identica/common/messaging/DefaultDeliveryServiceTest.java b/identica-common/src/test/java/me/whereareiam/identica/common/messaging/DefaultDeliveryServiceTest.java index 5a543f43d..46fbda309 100644 --- a/identica-common/src/test/java/me/whereareiam/identica/common/messaging/DefaultDeliveryServiceTest.java +++ b/identica-common/src/test/java/me/whereareiam/identica/common/messaging/DefaultDeliveryServiceTest.java @@ -33,7 +33,6 @@ void dispatchWaitsForRequiredServer() { DeliveryStore deliveryStore = mock(DeliveryStore.class); DefaultDeliveryService service = new DefaultDeliveryService( deliveryStore, - mock(me.whereareiam.identica.identity.IdentityService.class), mock(PipelineStateStore.class) ); TestIdentity identity = new TestIdentity(UUID.randomUUID(), UUID.randomUUID(), "PlayerOne"); diff --git a/identica-common/src/test/java/me/whereareiam/identica/common/migration/DefaultMigrationServiceTest.java b/identica-common/src/test/java/me/whereareiam/identica/common/migration/DefaultMigrationServiceTest.java index 3b9898b26..34fa5f76c 100644 --- a/identica-common/src/test/java/me/whereareiam/identica/common/migration/DefaultMigrationServiceTest.java +++ b/identica-common/src/test/java/me/whereareiam/identica/common/migration/DefaultMigrationServiceTest.java @@ -1,11 +1,10 @@ package me.whereareiam.identica.common.migration; -import me.whereareiam.identica.common.config.defaults.SettingsDefaults; +import com.google.inject.Provider; import me.whereareiam.identica.common.config.defaults.EngineDefaults; import me.whereareiam.identica.database.AccountPersistenceService; import me.whereareiam.identica.database.provider.ProviderLinkPersistenceService; import me.whereareiam.identica.event.EventManager; -import me.whereareiam.identica.type.ScenarioResolution; import me.whereareiam.identica.event.scenario.migration.MigrationRequiredEvent; import me.whereareiam.identica.event.scenario.migration.MigrationResolvedEvent; import me.whereareiam.identica.identity.IdentityService; @@ -14,7 +13,7 @@ import me.whereareiam.identica.model.config.Commands; import me.whereareiam.identica.model.config.Engine; import me.whereareiam.identica.model.config.Messages; -import me.whereareiam.identica.model.config.Settings; +import me.whereareiam.identica.model.config.provider.Providers; import me.whereareiam.identica.model.identity.Account; import me.whereareiam.identica.model.identity.provider.AccountProviderLink; import me.whereareiam.identica.model.migration.MigrationContext; @@ -23,16 +22,30 @@ import me.whereareiam.identica.model.migration.operation.MigrationRequest; import me.whereareiam.identica.model.migration.operation.MigrationResult; import me.whereareiam.identica.model.migration.operation.MigrationStart; +import me.whereareiam.identica.model.pipeline.journey.JourneyPlan; +import me.whereareiam.identica.model.pipeline.journey.stage.JourneyStage; +import me.whereareiam.identica.model.pipeline.journey.stage.step.JourneyStep; +import me.whereareiam.identica.model.pipeline.journey.stage.step.StepResult; import me.whereareiam.identica.model.pipeline.migration.MigrationPendingState; -import me.whereareiam.identica.model.pipeline.state.PipelineState; -import me.whereareiam.identica.model.pipeline.state.PipelineStateReference; +import me.whereareiam.identica.model.provider.InternalProvider; +import me.whereareiam.identica.model.provider.ProviderDescriptor; +import me.whereareiam.identica.pipeline.ScenarioContext; +import me.whereareiam.identica.pipeline.journey.registry.type.MigrationJourneyRegistry; +import me.whereareiam.identica.pipeline.journey.step.Step; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.PipelineStateReference; import me.whereareiam.identica.pipeline.state.PipelineStateStore; import me.whereareiam.identica.provider.ProviderManager; import me.whereareiam.identica.service.DeliveryService; -import me.whereareiam.identica.type.UsernameSource; +import me.whereareiam.identica.type.ScenarioResolution; import me.whereareiam.identica.type.migration.MigrationInitiator; import me.whereareiam.identica.type.migration.MigrationResultStatus; import me.whereareiam.identica.type.pipeline.PipelineType; +import me.whereareiam.identica.type.pipeline.journey.JourneyMode; +import me.whereareiam.identica.type.pipeline.journey.StageType; +import me.whereareiam.identica.type.pipeline.journey.step.StepContextRequirement; +import me.whereareiam.identica.type.provider.ProviderState; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -40,16 +53,11 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.time.Duration; -import java.util.List; -import java.util.Optional; -import java.util.UUID; +import java.util.*; import java.util.concurrent.CompletableFuture; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.argThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) @@ -71,16 +79,18 @@ class DefaultMigrationServiceTest { private DeliveryService deliveryService; @Mock private EventManager eventManager; + @Mock + private MigrationJourneyRegistry migrationJourneyRegistry; @DisplayName("Denies migration when the requested username is still occupied") @Test void deniesMigrationWhenUsernameNotFree() { doReturn(Optional.empty()).when(pipelineStateStore).find(any(PipelineStateReference.class)); + mockAvailableProviders("provider"); Account existing = Account.builder() .uniqueId(UUID.randomUUID()) .username("Player") - .source(UsernameSource.SYSTEM) .build(); when(accountPersistenceService.findByUsername("Player")).thenReturn(List.of(existing)); @@ -101,20 +111,7 @@ void deniesMigrationWhenUsernameNotFree() { behavior.setSessions(new Commands.Behavior.Sessions()); commands.setBehavior(behavior); - DefaultMigrationService service = new DefaultMigrationService( - providerManager, - providerLinkPersistenceService, - accountPersistenceService, - pipelineStateStore, - sessionService, - identityService, - deliveryService, - eventManager, - Settings::new, - this::engine, - () -> commands, - () -> messages - ); + DefaultMigrationService service = service(() -> providers(), () -> commands, () -> messages); UUID uniqueId = UUID.randomUUID(); MigrationResult result = service.start(MigrationStart.builder() @@ -132,23 +129,11 @@ void deniesMigrationWhenUsernameNotFree() { @Test void requestStoresAccountAndConnectionIdsInTheirOwnFields() { doReturn(Optional.empty()).when(pipelineStateStore).find(any(PipelineStateReference.class)); + mockAvailableProviders("premium"); when(providerLinkPersistenceService.findByUniqueIdAndProviderId(any(UUID.class), any(String.class))) .thenReturn(Optional.empty()); - DefaultMigrationService service = new DefaultMigrationService( - providerManager, - providerLinkPersistenceService, - accountPersistenceService, - pipelineStateStore, - sessionService, - identityService, - deliveryService, - eventManager, - Settings::new, - this::engine, - this::commands, - this::messages - ); + DefaultMigrationService service = service(); UUID connectionUniqueId = UUID.randomUUID(); UUID accountUniqueId = UUID.randomUUID(); @@ -194,20 +179,7 @@ void findPendingMigrationReturnsStartedMigrationFromPipelineState() { doReturn(Optional.of(pipelineState)).when(pipelineStateStore).find(any(PipelineStateReference.class)); - DefaultMigrationService service = new DefaultMigrationService( - providerManager, - providerLinkPersistenceService, - accountPersistenceService, - pipelineStateStore, - sessionService, - identityService, - deliveryService, - eventManager, - Settings::new, - this::engine, - this::commands, - this::messages - ); + DefaultMigrationService service = service(); PendingMigration pendingMigration = service.findPendingMigration(connectionUniqueId).orElse(null); @@ -225,6 +197,7 @@ void findPendingMigrationReturnsStartedMigrationFromPipelineState() { @Test void confirmStoresPendingMigrationEvenWhenTargetProviderAlreadyLinked() { doReturn(Optional.empty()).when(pipelineStateStore).find(any(PipelineStateReference.class)); + mockAvailableProviders("credential"); when(accountPersistenceService.findByUsername("PlayerOne")).thenReturn(List.of()); when(providerLinkPersistenceService.findByUniqueIdAndProviderId(any(UUID.class), any(String.class))) .thenReturn(Optional.of(AccountProviderLink.builder() @@ -236,21 +209,7 @@ void confirmStoresPendingMigrationEvenWhenTargetProviderAlreadyLinked() { when(sessionService.close(any(UUID.class))).thenReturn(CompletableFuture.completedFuture(null)); when(identityService.findByConnectionUniqueId(any(UUID.class))).thenReturn(Optional.empty()); - Settings settings = new SettingsDefaults().supply(new Settings()); - DefaultMigrationService service = new DefaultMigrationService( - providerManager, - providerLinkPersistenceService, - accountPersistenceService, - pipelineStateStore, - sessionService, - identityService, - deliveryService, - eventManager, - () -> settings, - this::engine, - this::commands, - Messages::new - ); + DefaultMigrationService service = service(() -> providers(), this::commands, Messages::new); UUID connectionUniqueId = UUID.randomUUID(); UUID accountUniqueId = UUID.randomUUID(); @@ -278,6 +237,46 @@ void confirmStoresPendingMigrationEvenWhenTargetProviderAlreadyLinked() { )); } + @DisplayName("Stores pending migration without precomputing a provider subject") + @Test + void confirmStoresPendingMigrationWithoutProviderContext() { + doReturn(Optional.empty()).when(pipelineStateStore).find(any(PipelineStateReference.class)); + mockAvailableProviders("credential"); + when(accountPersistenceService.findByUsername("PlayerOne")).thenReturn(List.of()); + when(providerLinkPersistenceService.findByUniqueIdAndProviderId(any(UUID.class), any(String.class))) + .thenReturn(Optional.empty()); + when(sessionService.close(any(UUID.class))).thenReturn(CompletableFuture.completedFuture(null)); + when(identityService.findByConnectionUniqueId(any(UUID.class))).thenReturn(Optional.empty()); + + DefaultMigrationService service = service(() -> providers(), this::commands, Messages::new); + + UUID connectionUniqueId = UUID.randomUUID(); + UUID accountUniqueId = UUID.randomUUID(); + service.request(MigrationRequest.builder() + .connectionUniqueId(connectionUniqueId) + .accountUniqueId(accountUniqueId) + .targetProviderId("credential") + .username("PlayerOne") + .ip("127.0.0.1") + .build()); + + MigrationResult confirmed = service.confirm(MigrationConfirm.builder() + .connectionUniqueId(connectionUniqueId) + .kickMessage("rejoin") + .build()); + + assertEquals(MigrationResultStatus.STARTED, confirmed.getStatus()); + + @SuppressWarnings("unchecked") + org.mockito.ArgumentCaptor stateCaptor = org.mockito.ArgumentCaptor.forClass(PipelineState.class); + verify(pipelineStateStore).save(any(PipelineStateReference.class), stateCaptor.capture(), anyLong()); + + MigrationContext stored = (MigrationContext) stateCaptor.getValue().getScenario(PipelineType.MIGRATION); + assertNotNull(stored); + assertNull(stored.getProvider()); + assertEquals("credential", stored.getTargetProviderId()); + } + @DisplayName("Queues a next-join notice when a started migration is cancelled") @Test void cancelPendingMigrationQueuesNextJoinNotice() { @@ -300,20 +299,7 @@ void cancelPendingMigrationQueuesNextJoinNotice() { doReturn(Optional.of(pipelineState)).when(pipelineStateStore).find(any(PipelineStateReference.class)); - DefaultMigrationService service = new DefaultMigrationService( - providerManager, - providerLinkPersistenceService, - accountPersistenceService, - pipelineStateStore, - sessionService, - identityService, - deliveryService, - eventManager, - Settings::new, - this::engine, - this::commands, - this::messages - ); + DefaultMigrationService service = service(); MigrationResult result = service.cancel(me.whereareiam.identica.model.migration.operation.MigrationCancel.builder() .connectionUniqueId(connectionUniqueId) @@ -333,6 +319,223 @@ void cancelPendingMigrationQueuesNextJoinNotice() { )); } + @DisplayName("Rejects request when the target provider is unknown") + @Test + void requestRejectsUnknownTargetProvider() { + doReturn(Optional.empty()).when(pipelineStateStore).find(any(PipelineStateReference.class)); + when(providerManager.getProviders()).thenReturn(List.of()); + + DefaultMigrationService service = service(); + + MigrationResult result = service.request(MigrationRequest.builder() + .connectionUniqueId(UUID.randomUUID()) + .accountUniqueId(UUID.randomUUID()) + .targetProviderId("ghost") + .username("PlayerOne") + .ip("127.0.0.1") + .build()); + + assertEquals(MigrationResultStatus.TARGET_UNSUPPORTED, result.getStatus()); + } + + @DisplayName("Rejects request when the target provider is configured but unavailable") + @Test + void requestRejectsUnavailableTargetProvider() { + doReturn(Optional.empty()).when(pipelineStateStore).find(any(PipelineStateReference.class)); + when(providerManager.getProviders()).thenReturn(List.of()); + + DefaultMigrationService service = service(() -> providers(providerEntry("premium", false)), this::commands, this::messages); + + MigrationResult result = service.request(MigrationRequest.builder() + .connectionUniqueId(UUID.randomUUID()) + .accountUniqueId(UUID.randomUUID()) + .targetProviderId("premium") + .username("PlayerOne") + .ip("127.0.0.1") + .build()); + + assertEquals(MigrationResultStatus.PROVIDER_UNAVAILABLE, result.getStatus()); + } + + @DisplayName("Rejects start when the target provider has no executable migration journey") + @Test + void startRejectsProviderWithoutMigrationJourney() { + when(providerManager.getProviders()).thenReturn(List.of(provider("premium"))); + when(migrationJourneyRegistry.resolvePlan(any(MigrationContext.class), eq(PipelineType.MIGRATION), any(JourneyMode.class), eq("premium"))) + .thenReturn(emptyJourneyPlan()); + + DefaultMigrationService service = service(); + + MigrationResult result = service.start(MigrationStart.builder() + .uniqueId(UUID.randomUUID()) + .connectionUniqueId(UUID.randomUUID()) + .targetProviderId("premium") + .username("PlayerOne") + .ip("127.0.0.1") + .build()); + + assertEquals(MigrationResultStatus.TARGET_UNSUPPORTED, result.getStatus()); + } + + @DisplayName("Confirm rejects and clears pending migration when target provider becomes unavailable") + @Test + void confirmRejectsUnavailableTargetProviderBeforeStoringPendingMigration() { + doReturn(Optional.empty()).when(pipelineStateStore).find(any(PipelineStateReference.class)); + when(providerManager.getProviders()).thenReturn(List.of(provider("premium")), List.of()); + when(migrationJourneyRegistry.resolvePlan(any(MigrationContext.class), eq(PipelineType.MIGRATION), any(JourneyMode.class), eq("premium"))) + .thenReturn(journeyPlanWithProviderSteps()); + when(providerLinkPersistenceService.findByUniqueIdAndProviderId(any(UUID.class), any(String.class))) + .thenReturn(Optional.empty()); + + DefaultMigrationService service = service(() -> providers(providerEntry("premium", true)), this::commands, this::messages); + + UUID connectionUniqueId = UUID.randomUUID(); + MigrationResult requested = service.request(MigrationRequest.builder() + .connectionUniqueId(connectionUniqueId) + .accountUniqueId(UUID.randomUUID()) + .targetProviderId("premium") + .username("PlayerOne") + .ip("127.0.0.1") + .build()); + assertEquals(MigrationResultStatus.PENDING_CONFIRMATION, requested.getStatus()); + + MigrationResult confirmed = service.confirm(MigrationConfirm.builder() + .connectionUniqueId(connectionUniqueId) + .kickMessage("rejoin") + .build()); + + assertEquals(MigrationResultStatus.PROVIDER_UNAVAILABLE, confirmed.getStatus()); + assertTrue(service.findPendingMigration(connectionUniqueId).isEmpty()); + verify(pipelineStateStore, never()).save(any(PipelineStateReference.class), any(PipelineState.class), anyLong()); + } + + private DefaultMigrationService service() { + return service(this::providers, this::commands, this::messages); + } + + private DefaultMigrationService service( + Provider providersProvider, + Provider commandsProvider, + Provider messagesProvider + ) { + return new DefaultMigrationService( + providerManager, + migrationJourneyRegistry, + providerLinkPersistenceService, + accountPersistenceService, + pipelineStateStore, + sessionService, + identityService, + deliveryService, + eventManager, + this::engine, + providersProvider, + commandsProvider, + messagesProvider + ); + } + + private void mockAvailableProviders(String... providerIds) { + when(providerManager.getProviders()).thenReturn(Arrays.stream(providerIds).map(this::provider).toList()); + when(migrationJourneyRegistry.resolvePlan(any(MigrationContext.class), eq(PipelineType.MIGRATION), any(JourneyMode.class), anyString())) + .thenReturn(journeyPlanWithProviderSteps()); + } + + private InternalProvider provider(String id) { + return InternalProvider.builder() + .descriptor(descriptor(id)) + .state(ProviderState.ENABLED) + .build(); + } + + private ProviderDescriptor descriptor(String id) { + ProviderDescriptor descriptor = new ProviderDescriptor(); + descriptor.setId(id); + descriptor.setName(id); + descriptor.setVersion("1.0.0"); + descriptor.setMain("ignored.Main"); + descriptor.setSupportedPlatforms(List.of("ANY")); + return descriptor; + } + + private Providers providers(Providers.ProviderEntry... entries) { + Providers providers = new Providers(); + providers.setProviders(List.of(entries)); + return providers; + } + + private Providers providers() { + return providers(new Providers.ProviderEntry[0]); + } + + private Providers.ProviderEntry providerEntry(String id, boolean enabled) { + Providers.ProviderEntry entry = new Providers.ProviderEntry(); + entry.setId(id); + entry.setEnabled(enabled); + return entry; + } + + private JourneyPlan journeyPlanWithProviderSteps() { + JourneyStage stage = JourneyStage.builder() + .id(StageType.PROVIDER.id()) + .type(StageType.PROVIDER) + .order(100) + .pipelineTypes(EnumSet.of(PipelineType.MIGRATION)) + .journeyModes(EnumSet.allOf(JourneyMode.class)) + .build(); + + return new JourneyPlan(List.of(new JourneyPlan.StageEntry( + stage, + List.of(JourneyStep.builder() + .stageId(StageType.PROVIDER.id()) + .step(new TestStep("migration-step")) + .providerId("premium") + .scenarios(EnumSet.of(PipelineType.MIGRATION)) + .journeyModes(EnumSet.of(JourneyMode.SEAMLESS)) + .build()) + ))); + } + + private JourneyPlan emptyJourneyPlan() { + JourneyStage stage = JourneyStage.builder() + .id(StageType.PROVIDER.id()) + .type(StageType.PROVIDER) + .order(100) + .pipelineTypes(EnumSet.of(PipelineType.MIGRATION)) + .journeyModes(EnumSet.allOf(JourneyMode.class)) + .build(); + + return new JourneyPlan(List.of(new JourneyPlan.StageEntry(stage, List.of()))); + } + + private static final class TestStep implements Step { + private final String name; + + private TestStep(String name) { + this.name = name; + } + + @Override + public @NotNull String getName() { + return name; + } + + @Override + public @NotNull Set journeyModes() { + return Set.of(JourneyMode.SEAMLESS); + } + + @Override + public @NotNull StepContextRequirement contextRequirement() { + return StepContextRequirement.LOGIN; + } + + @Override + public @NotNull CompletableFuture execute(@NotNull ScenarioContext context) { + return CompletableFuture.completedFuture(null); + } + } + private Commands commands() { Commands commands = new Commands(); Commands.Behavior behavior = new Commands.Behavior(); diff --git a/identica-common/src/test/java/me/whereareiam/identica/common/provider/DefaultProviderJoinRestrictionServiceTest.java b/identica-common/src/test/java/me/whereareiam/identica/common/provider/DefaultProviderJoinRestrictionServiceTest.java deleted file mode 100644 index 1dda9c44d..000000000 --- a/identica-common/src/test/java/me/whereareiam/identica/common/provider/DefaultProviderJoinRestrictionServiceTest.java +++ /dev/null @@ -1,345 +0,0 @@ -package me.whereareiam.identica.common.provider; - -import me.whereareiam.identica.common.provider.restriction.DefaultProviderJoinRestrictionService; -import me.whereareiam.identica.common.provider.restriction.ProviderJoinRestrictionToggleStore; -import me.whereareiam.identica.common.replication.DefaultReplicationSystem; -import me.whereareiam.identica.common.replication.ReplicationTestFixtures; -import me.whereareiam.identica.database.provider.ProviderLinkPersistenceService; -import me.whereareiam.identica.identity.actor.ConnectionIdentity; -import me.whereareiam.identica.identity.session.recognition.SessionRecognitionService; -import me.whereareiam.identica.model.config.Replication; -import me.whereareiam.identica.model.config.provider.Providers; -import me.whereareiam.identica.model.identity.provider.AccountProviderLink; -import me.whereareiam.identica.model.provider.restriction.ProviderJoinRestrictionDecision; -import me.whereareiam.identica.model.provider.restriction.ProviderJoinRestrictionStatus; -import me.whereareiam.identica.type.provider.ProviderJoinRestrictionCondition; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import java.time.Duration; -import java.util.*; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -@DisplayName("Default Provider Join-Restriction Service") -class DefaultProviderJoinRestrictionServiceTest { - private static final ConnectionIdentity.Origin ORIGIN = - new ConnectionIdentity.Origin("premium.arcadeya.com", 25565); - - @DisplayName("Enable activates configured provider restriction") - @Test - void enableActivatesConfiguredRestriction() { - DefaultProviderJoinRestrictionService service = new DefaultProviderJoinRestrictionService( - this::providers, - new TestToggleStore(), - mock(SessionRecognitionService.class), - mock(ProviderLinkPersistenceService.class) - ); - - ProviderJoinRestrictionStatus status = service.enable("premium"); - - assertTrue(status.isActive()); - assertEquals(Set.of(ProviderJoinRestrictionCondition.RECOGNIZED), status.getAllow()); - } - - @DisplayName("Enable does not activate unconfigured provider restriction") - @Test - void enableFailsWithoutConfiguredRestriction() { - DefaultProviderJoinRestrictionService service = new DefaultProviderJoinRestrictionService( - this::providers, - new TestToggleStore(), - mock(SessionRecognitionService.class), - mock(ProviderLinkPersistenceService.class) - ); - - ProviderJoinRestrictionStatus status = service.enable("credential"); - - assertFalse(status.isActive()); - assertTrue(status.getAllow().isEmpty()); - } - - @DisplayName("Provider-only evaluation allows when restriction is inactive") - @Test - void providerOnlyEvaluationAllowsWhenInactive() { - TestToggleStore store = new TestToggleStore(); - DefaultProviderJoinRestrictionService service = new DefaultProviderJoinRestrictionService( - this::providers, - store, - mock(SessionRecognitionService.class), - mock(ProviderLinkPersistenceService.class) - ); - - ProviderJoinRestrictionDecision decision = service.evaluate("premium"); - - assertTrue(decision.isAllowed()); - assertFalse(decision.isActive()); - assertTrue(decision.isConfigured()); - assertEquals(Set.of(ProviderJoinRestrictionCondition.RECOGNIZED), decision.getAllow()); - } - - @DisplayName("Provider-only evaluation allows when restriction config is absent") - @Test - void providerOnlyEvaluationAllowsWhenRestrictionIsMissing() { - DefaultProviderJoinRestrictionService service = new DefaultProviderJoinRestrictionService( - this::providers, - new TestToggleStore(), - mock(SessionRecognitionService.class), - mock(ProviderLinkPersistenceService.class) - ); - - ProviderJoinRestrictionDecision decision = service.evaluate("missing"); - - assertTrue(decision.isAllowed()); - assertFalse(decision.isActive()); - assertFalse(decision.isConfigured()); - assertTrue(decision.getAllow().isEmpty()); - } - - @DisplayName("Provider-only evaluation denies active restrictions that require context") - @Test - void providerOnlyEvaluationDeniesContextDependentRestrictions() { - TestToggleStore store = new TestToggleStore(); - DefaultProviderJoinRestrictionService service = new DefaultProviderJoinRestrictionService( - this::providers, - store, - mock(SessionRecognitionService.class), - mock(ProviderLinkPersistenceService.class) - ); - - store.enable("premium"); - ProviderJoinRestrictionDecision decision = service.evaluate("premium"); - - assertFalse(decision.isAllowed()); - assertTrue(decision.isActive()); - assertTrue(decision.isConfigured()); - assertEquals(Set.of(ProviderJoinRestrictionCondition.RECOGNIZED), decision.getAllow()); - assertTrue(decision.getMatchedConditions().isEmpty()); - } - - @DisplayName("Provider-only evaluation denies active deny-all restrictions") - @Test - void providerOnlyEvaluationDeniesEmptyAllowSet() { - TestToggleStore store = new TestToggleStore(); - DefaultProviderJoinRestrictionService service = new DefaultProviderJoinRestrictionService( - this::providers, - store, - mock(SessionRecognitionService.class), - mock(ProviderLinkPersistenceService.class) - ); - - store.enable("deny-all"); - ProviderJoinRestrictionDecision decision = service.evaluate("deny-all"); - - assertFalse(decision.isAllowed()); - assertTrue(decision.isActive()); - assertTrue(decision.isConfigured()); - assertTrue(decision.getAllow().isEmpty()); - assertTrue(decision.getMatchedConditions().isEmpty()); - } - - @DisplayName("Full evaluation allows recognized sessions") - @Test - void fullEvaluationAllowsRecognizedSessions() { - TestToggleStore store = new TestToggleStore(); - SessionRecognitionService recognitionService = mock(SessionRecognitionService.class); - when(recognitionService.matches("premium", "subject", "Player", "1.1.1.1", ORIGIN)) - .thenReturn(true); - DefaultProviderJoinRestrictionService service = new DefaultProviderJoinRestrictionService( - this::providers, - store, - recognitionService, - mock(ProviderLinkPersistenceService.class) - ); - - store.enable("premium"); - ProviderJoinRestrictionDecision decision = service.evaluate("premium", "subject", "Player", "1.1.1.1", ORIGIN); - - assertTrue(decision.isAllowed()); - assertEquals(Set.of(ProviderJoinRestrictionCondition.RECOGNIZED), decision.getMatchedConditions()); - } - - @DisplayName("Full evaluation allows linked accounts") - @Test - void fullEvaluationAllowsLinkedAccounts() { - TestToggleStore store = new TestToggleStore(); - SessionRecognitionService recognitionService = mock(SessionRecognitionService.class); - when(recognitionService.matches("linked-only", "subject", "Player", "1.1.1.1", ORIGIN)) - .thenReturn(false); - ProviderLinkPersistenceService linkPersistenceService = mock(ProviderLinkPersistenceService.class); - when(linkPersistenceService.findBySubject("linked-only", "subject")) - .thenReturn(Optional.of(AccountProviderLink.builder() - .uniqueId(UUID.randomUUID()) - .providerId("linked-only") - .providerSubject("subject") - .primaryLink(true) - .build())); - DefaultProviderJoinRestrictionService service = new DefaultProviderJoinRestrictionService( - this::providers, - store, - recognitionService, - linkPersistenceService - ); - - store.enable("linked-only"); - ProviderJoinRestrictionDecision decision = service.evaluate("linked-only", "subject", "Player", "1.1.1.1", ORIGIN); - - assertTrue(decision.isAllowed()); - assertEquals(Set.of(ProviderJoinRestrictionCondition.LINKED), decision.getMatchedConditions()); - } - - @DisplayName("Full evaluation allows when any allowed condition matches") - @Test - void fullEvaluationAllowsAnyMatchedCondition() { - TestToggleStore store = new TestToggleStore(); - SessionRecognitionService recognitionService = mock(SessionRecognitionService.class); - when(recognitionService.matches("combined", "subject", "Player", "1.1.1.1", ORIGIN)) - .thenReturn(false); - ProviderLinkPersistenceService linkPersistenceService = mock(ProviderLinkPersistenceService.class); - when(linkPersistenceService.findBySubject("combined", "subject")) - .thenReturn(Optional.of(AccountProviderLink.builder() - .uniqueId(UUID.randomUUID()) - .providerId("combined") - .providerSubject("subject") - .primaryLink(true) - .build())); - DefaultProviderJoinRestrictionService service = new DefaultProviderJoinRestrictionService( - this::providers, - store, - recognitionService, - linkPersistenceService - ); - - store.enable("combined"); - ProviderJoinRestrictionDecision decision = service.evaluate("combined", "subject", "Player", "1.1.1.1", ORIGIN); - - assertTrue(decision.isAllowed()); - assertEquals(Set.of(ProviderJoinRestrictionCondition.LINKED), decision.getMatchedConditions()); - } - - @DisplayName("Full evaluation denies when no condition matches") - @Test - void fullEvaluationDeniesWhenNoConditionMatches() { - TestToggleStore store = new TestToggleStore(); - SessionRecognitionService recognitionService = mock(SessionRecognitionService.class); - when(recognitionService.matches("premium", "subject", "Player", "1.1.1.1", ORIGIN)) - .thenReturn(false); - DefaultProviderJoinRestrictionService service = new DefaultProviderJoinRestrictionService( - this::providers, - store, - recognitionService, - mock(ProviderLinkPersistenceService.class) - ); - - store.enable("premium"); - ProviderJoinRestrictionDecision decision = service.evaluate("premium", "subject", "Player", "1.1.1.1", ORIGIN); - - assertFalse(decision.isAllowed()); - assertTrue(decision.getMatchedConditions().isEmpty()); - } - - @DisplayName("Full evaluation denies linked restriction when subject is missing") - @Test - void fullEvaluationDeniesLinkedRestrictionWithoutSubject() { - TestToggleStore store = new TestToggleStore(); - SessionRecognitionService recognitionService = mock(SessionRecognitionService.class); - when(recognitionService.matches("linked-only", null, "Player", "1.1.1.1", ORIGIN)) - .thenReturn(false); - DefaultProviderJoinRestrictionService service = new DefaultProviderJoinRestrictionService( - this::providers, - store, - recognitionService, - mock(ProviderLinkPersistenceService.class) - ); - - store.enable("linked-only"); - ProviderJoinRestrictionDecision decision = service.evaluate("linked-only", null, "Player", "1.1.1.1", ORIGIN); - - assertFalse(decision.isAllowed()); - assertTrue(decision.getMatchedConditions().isEmpty()); - } - - private Providers providers() { - Providers providers = new Providers(); - - Providers.ProviderEntry premium = new Providers.ProviderEntry(); - premium.setId("premium"); - premium.setRestriction(restriction(true, List.of(ProviderJoinRestrictionCondition.RECOGNIZED))); - - Providers.ProviderEntry credential = new Providers.ProviderEntry(); - credential.setId("credential"); - credential.setRestriction(restriction(false, null)); - - Providers.ProviderEntry denyAll = new Providers.ProviderEntry(); - denyAll.setId("deny-all"); - denyAll.setRestriction(restriction(true, List.of())); - - Providers.ProviderEntry linkedOnly = new Providers.ProviderEntry(); - linkedOnly.setId("linked-only"); - linkedOnly.setRestriction(restriction(true, List.of(ProviderJoinRestrictionCondition.LINKED))); - - Providers.ProviderEntry combined = new Providers.ProviderEntry(); - combined.setId("combined"); - combined.setRestriction(restriction(true, List.of( - ProviderJoinRestrictionCondition.RECOGNIZED, - ProviderJoinRestrictionCondition.LINKED - ))); - - Providers.ProviderEntry missing = new Providers.ProviderEntry(); - missing.setId("missing"); - - providers.setProviders(List.of(premium, credential, denyAll, linkedOnly, combined, missing)); - return providers; - } - - private Providers.ProviderEntry.Restriction restriction( - boolean enabled, - List allow - ) { - Providers.ProviderEntry.Restriction restriction = new Providers.ProviderEntry.Restriction(); - Providers.ProviderEntry.Restriction.Join join = new Providers.ProviderEntry.Restriction.Join(); - join.setEnabled(enabled); - join.setAllow(allow); - restriction.setJoin(join); - return restriction; - } - - private static final class TestToggleStore extends ProviderJoinRestrictionToggleStore { - private final Set active = new HashSet<>(); - - private TestToggleStore() { - super( - new DefaultReplicationSystem(new ReplicationTestFixtures.TestReplicationAdapter()), () -> { - Replication replication = new Replication(); - Replication.Cache cache = new Replication.Cache(); - cache.setProviderJoinRestrictions("provider-join-restrictions"); - replication.setCache(cache); - return replication; - }, - () -> { - Providers settings = new Providers(); - Providers.Behavior behavior = new Providers.Behavior(); - behavior.setJoinRestrictionToggleTtl(Duration.ofDays(365)); - settings.setBehavior(behavior); - return settings; - } - ); - } - - @Override - public boolean isActive(String providerId) { - return active.contains(providerId.trim().toLowerCase(Locale.ROOT)); - } - - @Override - public void enable(String providerId) { - active.add(providerId.trim().toLowerCase(Locale.ROOT)); - } - - @Override - public void disable(String providerId) { - active.remove(providerId.trim().toLowerCase(Locale.ROOT)); - } - } -} diff --git a/identica-common/src/test/java/me/whereareiam/identica/common/provider/DefaultProviderOperationsTest.java b/identica-common/src/test/java/me/whereareiam/identica/common/provider/DefaultProviderOperationsTest.java index 73c6a3cd0..2b8350f60 100644 --- a/identica-common/src/test/java/me/whereareiam/identica/common/provider/DefaultProviderOperationsTest.java +++ b/identica-common/src/test/java/me/whereareiam/identica/common/provider/DefaultProviderOperationsTest.java @@ -19,12 +19,16 @@ import me.whereareiam.identica.pipeline.journey.step.Step; import me.whereareiam.identica.provider.ProviderManager; import me.whereareiam.identica.provider.ProviderOperations; +import me.whereareiam.identica.provider.profile.ProfileResolution; +import me.whereareiam.identica.provider.profile.ProfileResolveContext; +import me.whereareiam.identica.provider.profile.ProfileSubjectResolver; import me.whereareiam.identica.type.pipeline.PipelineType; import me.whereareiam.identica.type.pipeline.journey.JourneyMode; import me.whereareiam.identica.type.pipeline.journey.StageType; import me.whereareiam.identica.type.pipeline.journey.step.StepContextRequirement; import me.whereareiam.identica.type.provider.ProviderState; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -144,6 +148,41 @@ void keepsAutomaticEligibilityOnUntrustedIp() { assertTrue(operations(providers).isEligible(autoContext(), provider, PipelineType.AUTHENTICATION, JourneyMode.INTERACTIVE)); } + @DisplayName("Returns no profile resolution when no provider resolver applies") + @Test + void resolveProfileReturnsNullWithoutProviderResolvers() { + when(providerManager.getProviders()).thenReturn(List.of()); + + ProfileResolution resolution = operations(new Providers()).resolveProfile(ProfileResolveContext.builder() + .identity(new ConnectionIdentity(UUID.randomUUID(), "PlayerOne", "127.0.0.1")) + .build()); + + assertNull(resolution); + } + + @DisplayName("Uses provider-registered profile resolvers to resolve subjects") + @Test + void resolveProfileUsesProviderResolvers() { + ProviderDescriptor descriptor = new ProviderDescriptor(); + descriptor.setId("credential"); + + InternalProvider provider = InternalProvider.builder() + .descriptor(descriptor) + .priority(10) + .state(ProviderState.ENABLED) + .profileSubjectResolvers(Set.of(new StaticProfileResolver())) + .build(); + when(providerManager.getProviders()).thenReturn(List.of(provider)); + + ProfileResolution resolution = operations(new Providers()).resolveProfile(ProfileResolveContext.builder() + .identity(new ConnectionIdentity(UUID.randomUUID(), "PlayerOne", "127.0.0.1")) + .build()); + + assertNotNull(resolution); + assertEquals("credential", resolution.getProviderId()); + assertEquals("credential-subject", resolution.getProviderSubject()); + } + private ProviderOperations operations(Providers providers) { return new DefaultProviderOperations( providerManager, @@ -249,4 +288,19 @@ private static final class NoopStep implements Step { return CompletableFuture.completedFuture(StepResult.proceed(context)); } } + + private static final class StaticProfileResolver implements ProfileSubjectResolver { + @Override + public boolean supports(@NotNull ProfileResolveContext context) { + return true; + } + + @Override + public @Nullable ProfileResolution resolve(@NotNull ProfileResolveContext context) { + return ProfileResolution.builder() + .providerId("credential") + .providerSubject("credential-subject") + .build(); + } + } } diff --git a/identica-common/src/test/java/me/whereareiam/identica/common/provider/ProviderJoinRestrictionToggleStoreTest.java b/identica-common/src/test/java/me/whereareiam/identica/common/provider/ProviderJoinRestrictionToggleStoreTest.java deleted file mode 100644 index 8ea7d1ee6..000000000 --- a/identica-common/src/test/java/me/whereareiam/identica/common/provider/ProviderJoinRestrictionToggleStoreTest.java +++ /dev/null @@ -1,100 +0,0 @@ -package me.whereareiam.identica.common.provider; - -import me.whereareiam.identica.common.provider.restriction.ProviderJoinRestrictionToggleStore; -import me.whereareiam.identica.common.replication.DefaultReplicationSystem; -import me.whereareiam.identica.model.config.Replication; -import me.whereareiam.identica.model.config.provider.Providers; -import me.whereareiam.identica.model.replication.ReplicationPage; -import me.whereareiam.identica.replication.ReplicationAdapter; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -@DisplayName("Provider Join-Restriction Toggle Store") -class ProviderJoinRestrictionToggleStoreTest { - @DisplayName("Enable and disable are normalized by provider id") - @Test - void enableAndDisableNormalizeProviderId() { - ProviderJoinRestrictionToggleStore store = new ProviderJoinRestrictionToggleStore( - new DefaultReplicationSystem(new InMemoryReplicationAdapter()), - this::replication, - this::settings - ); - - store.enable("Premium"); - assertTrue(store.isActive("premium")); - assertTrue(store.isActive(" PREMIUM ")); - - store.disable("premium"); - assertFalse(store.isActive("Premium")); - } - - private Replication replication() { - Replication replication = new Replication(); - Replication.Cache cache = new Replication.Cache(); - cache.setProviderJoinRestrictions("provider-join-restrictions"); - replication.setCache(cache); - return replication; - } - - private Providers settings() { - Providers settings = new Providers(); - Providers.Behavior behavior = new Providers.Behavior(); - behavior.setJoinRestrictionToggleTtl(java.time.Duration.ofDays(365)); - settings.setBehavior(behavior); - return settings; - } - - private static final class InMemoryReplicationAdapter implements ReplicationAdapter { - private final Map entries = new ConcurrentHashMap<>(); - - @Override - public boolean isAvailable() { - return true; - } - - @Override - public @NotNull CompletableFuture> get(@NotNull String namespace, @NotNull String key) { - return CompletableFuture.completedFuture(Optional.ofNullable(entries.get(namespace + ":" + key))); - } - - @Override - public @NotNull CompletableFuture> consume(@NotNull String namespace, @NotNull String key) { - return CompletableFuture.completedFuture(Optional.ofNullable(entries.remove(namespace + ":" + key))); - } - - @Override - public @NotNull CompletableFuture put(@NotNull String namespace, @NotNull String key, byte[] value, long ttlMs) { - entries.put(namespace + ":" + key, value); - return CompletableFuture.completedFuture(null); - } - - @Override - public @NotNull CompletableFuture invalidate(@NotNull String namespace, @NotNull String key) { - entries.remove(namespace + ":" + key); - return CompletableFuture.completedFuture(null); - } - - @Override - public @NotNull CompletableFuture listKeys(@NotNull String namespace, int page, int pageSize) { - return CompletableFuture.completedFuture(ReplicationPage.empty(page, pageSize)); - } - - @Override - public @NotNull CompletableFuture publish(@NotNull String channel, byte[] payload) { - return CompletableFuture.completedFuture(null); - } - - @Override - public void subscribe(@NotNull String channel, @NotNull java.util.function.Consumer handler) { - } - } -} diff --git a/identica-common/src/test/java/me/whereareiam/identica/common/provider/ProviderLifecycleControllerConfigBootstrapTest.java b/identica-common/src/test/java/me/whereareiam/identica/common/provider/ProviderLifecycleControllerConfigBootstrapTest.java index 52d910438..bfd6eaef3 100644 --- a/identica-common/src/test/java/me/whereareiam/identica/common/provider/ProviderLifecycleControllerConfigBootstrapTest.java +++ b/identica-common/src/test/java/me/whereareiam/identica/common/provider/ProviderLifecycleControllerConfigBootstrapTest.java @@ -2,6 +2,7 @@ import com.google.inject.*; import com.google.inject.Module; +import com.google.inject.multibindings.Multibinder; import com.google.inject.name.Named; import lombok.Getter; import lombok.Setter; @@ -10,9 +11,16 @@ import me.whereareiam.configura.merge.defaults.DefaultsProvider; import me.whereareiam.identica.Registry; import me.whereareiam.identica.Reloadable; -import me.whereareiam.identica.common.provider.dependency.ProviderDependencyResolver; -import me.whereareiam.identica.common.provider.factory.ProviderClassLoaderFactory; +import me.whereareiam.identica.common.provider.capability.DefaultProviderCapabilityCoordinator; +import me.whereareiam.identica.common.provider.capability.DefaultProviderCapabilityRegistry; +import me.whereareiam.identica.common.provider.classloader.ProviderRuntimeClassLoaderFactory; +import me.whereareiam.identica.common.provider.classloader.SharedCapabilityClassLoaderFactory; +import me.whereareiam.identica.common.provider.dependency.ProviderDependencyLoggingAdapter; import me.whereareiam.identica.common.provider.factory.ProviderInstanceFactory; +import me.whereareiam.identica.common.provider.injector.ProviderInjectorFactory; +import me.whereareiam.identica.common.provider.library.ProviderLibraryInstaller; +import me.whereareiam.identica.common.provider.library.ProviderLibraryPlanner; +import me.whereareiam.identica.common.provider.library.SharedLibraryConflictTracker; import me.whereareiam.identica.common.provider.resolver.ProviderResolverRegistry; import me.whereareiam.identica.common.provider.resolver.ProviderWorkingPathResolver; import me.whereareiam.identica.common.registry.ReloadableRegistry; @@ -35,6 +43,8 @@ import me.whereareiam.identica.provider.IdenticaProvider; import me.whereareiam.identica.provider.ProviderPlatformBinding; import me.whereareiam.identica.provider.ProviderPlatformExtension; +import me.whereareiam.identica.provider.capability.ProviderCapabilityCoordinator; +import me.whereareiam.identica.provider.capability.ProviderCapabilityRegistry; import me.whereareiam.identica.type.event.EventOrder; import me.whereareiam.identica.type.provider.ProviderState; import org.jetbrains.annotations.NotNull; @@ -43,7 +53,6 @@ import org.junit.jupiter.api.io.TempDir; import java.lang.reflect.Method; -import java.net.URL; import java.net.URLClassLoader; import java.nio.file.Files; import java.nio.file.Path; @@ -96,15 +105,15 @@ void enablingAndDisablingAProviderRegistersPlatformBindings(@TempDir Path tempDi private static InternalProvider discoveredProvider() { return InternalProvider.builder() .path(Path.of("ignored.jar")) - .descriptor(descriptor("test-provider", "Test Provider")) + .descriptor(descriptor()) .state(ProviderState.DISCOVERED) .build(); } - private static ProviderDescriptor descriptor(String id, String name) { + private static ProviderDescriptor descriptor() { ProviderDescriptor descriptor = new ProviderDescriptor(); - descriptor.setId(id); - descriptor.setName(name); + descriptor.setId("test-provider"); + descriptor.setName("Test Provider"); descriptor.setVersion("1.0.0"); descriptor.setMain(TestProvider.class.getName()); descriptor.setSupportedPlatforms(List.of("any")); @@ -124,7 +133,9 @@ protected void configure() { bind(ProviderWorkingPathResolver.class).asEagerSingleton(); bind(ProviderInstanceFactory.class).asEagerSingleton(); bind(ProviderLifecycleController.class).asEagerSingleton(); - bind(new com.google.inject.TypeLiteral>() {}).to(ReloadableRegistry.class).asEagerSingleton(); + bind(ProviderCapabilityCoordinator.class).to(DefaultProviderCapabilityCoordinator.class).asEagerSingleton(); + bind(ProviderCapabilityRegistry.class).to(DefaultProviderCapabilityRegistry.class).asEagerSingleton(); + bind(new TypeLiteral>() {}).to(ReloadableRegistry.class).asEagerSingleton(); bind(ConflictService.class).toInstance(new NoopConflictService()); bind(EventManager.class).toInstance(new NoopEventManager()); bind(HandshakeStore.class).toInstance(new NoopHandshakeStore()); @@ -132,7 +143,7 @@ protected void configure() { bind(ProviderResolverRegistry.class).toInstance(new ProviderResolverRegistry()); } - @com.google.inject.Provides + @Provides @Singleton @Named("providersPath") Path provideProvidersPath() throws Exception { @@ -141,41 +152,54 @@ Path provideProvidersPath() throws Exception { return providersPath; } - @com.google.inject.Provides + @Provides @Singleton - ProviderClassLoaderFactory provideClassLoaderFactory() { - return new ProviderClassLoaderFactory() { + ProviderRuntimeClassLoaderFactory provideProviderRuntimeClassLoaderFactory() { + return new ProviderRuntimeClassLoaderFactory(new SharedCapabilityClassLoaderFactory()) { @Override public URLClassLoader create(Path jarPath) { - return new URLClassLoader(new URL[0], getClass().getClassLoader()); + return new URLClassLoader(new java.net.URL[0], getClass().getClassLoader()); } }; } - @com.google.inject.Provides + @Provides @Singleton - ProviderDependencyResolver provideDependencyResolver(@Named("providersPath") Path providersPath) { - return new ProviderDependencyResolver(providersPath, null) { + ProviderLibraryInstaller provideProviderLibraryInstaller() { + return new ProviderLibraryInstaller( + tempDir.resolve("providers"), + tempDir.resolve("capabilities"), + org.mockito.Mockito.mock(ProviderDependencyLoggingAdapter.class), + new SharedCapabilityClassLoaderFactory() + ) { @Override - public void loadDescriptorLibraries(ProviderDescriptor descriptor, ClassLoader classLoader) { - } - - @Override - public void loadProviderLibraries(ProviderDescriptor descriptor, IdenticaProvider provider, ClassLoader classLoader) { + protected void install( + @NotNull Path basePath, + @NotNull String cacheNamespace, + ProviderLibraries libraries, + @NotNull URLClassLoader classLoader + ) { } }; } - @com.google.inject.Provides + @Provides + @Singleton + ProviderLibraryPlanner provideProviderLibraryPlanner() { + return new ProviderLibraryPlanner(new SharedLibraryConflictTracker()); + } + + @Provides @Singleton - me.whereareiam.identica.common.provider.injector.ProviderInjectorFactory provideInjectorFactory() { - return new me.whereareiam.identica.common.provider.injector.ProviderInjectorFactory(null) { + ProviderInjectorFactory provideInjectorFactory() { + return new ProviderInjectorFactory(null) { @Override - public com.google.inject.Injector create( + public Injector create( Path workingPath, ProviderDescriptor descriptor, IdenticaProvider probeProvider, - ProviderPlatformExtension probePlatformExtension + ProviderPlatformExtension probePlatformExtension, + @NotNull List capabilityModules ) { return Guice.createInjector(new ProviderRootModule(), new ProviderRuntimeModule(workingPath, probeProvider)); } @@ -186,7 +210,7 @@ public com.google.inject.Injector create( private static final class ProviderRootModule extends AbstractModule { @Override protected void configure() { - bind(new com.google.inject.TypeLiteral>() {}).to(ReloadableRegistry.class).asEagerSingleton(); + bind(new TypeLiteral>() {}).to(ReloadableRegistry.class).asEagerSingleton(); } } @@ -206,14 +230,14 @@ protected void configure() { install(module); } - @com.google.inject.Provides + @Provides @Singleton @Named("workingPath") Path provideWorkingPath() { return workingPath; } - @com.google.inject.Provides + @Provides @Singleton ProviderDescriptor provideDescriptor() { ProviderDescriptor descriptor = new ProviderDescriptor(); @@ -240,7 +264,7 @@ protected void configure() { bind(TestSettingsProvider.class).asEagerSingleton(); bind(TestMessagesProvider.class).asEagerSingleton(); bind(TestCommandsProvider.class).asEagerSingleton(); - com.google.inject.multibindings.Multibinder.newSetBinder(binder(), ProviderPlatformBinding.class) + Multibinder.newSetBinder(binder(), ProviderPlatformBinding.class) .addBinding() .to(ProbePlatformBinding.class); } @@ -343,20 +367,20 @@ public ConflictResolver getResolver(@NotNull String id) { } @Override - public void register(@NotNull ConflictType type) { + public void register(@NotNull ConflictType type) { } @Override - public void unregister(@NotNull ConflictType type) { + public void unregister(@NotNull ConflictType type) { } @Override - public ConflictType getType(@NotNull String key) { + public ConflictType getType(@NotNull String key) { return null; } @Override - public @NotNull Set getTypes() { + public @NotNull Set> getTypes() { return Set.of(); } diff --git a/identica-common/src/test/java/me/whereareiam/identica/common/provider/capability/DefaultProviderCapabilityCoordinatorTest.java b/identica-common/src/test/java/me/whereareiam/identica/common/provider/capability/DefaultProviderCapabilityCoordinatorTest.java new file mode 100644 index 000000000..6dfcacf45 --- /dev/null +++ b/identica-common/src/test/java/me/whereareiam/identica/common/provider/capability/DefaultProviderCapabilityCoordinatorTest.java @@ -0,0 +1,229 @@ +package me.whereareiam.identica.common.provider.capability; + +import com.google.inject.*; +import com.google.inject.Module; +import com.google.inject.multibindings.Multibinder; +import com.google.inject.name.Named; +import me.whereareiam.identica.common.provider.classloader.SharedCapabilityClassLoaderFactory; +import me.whereareiam.identica.model.provider.InternalProvider; +import me.whereareiam.identica.model.provider.ProviderDescriptor; +import me.whereareiam.identica.model.provider.capability.ProviderCapabilityDeclaration; +import me.whereareiam.identica.model.provider.capability.ProviderCapabilityInstallation; +import me.whereareiam.identica.provider.capability.ProviderCapabilityCoordinator; +import me.whereareiam.identica.provider.capability.ProviderCapabilityRegistry; +import me.whereareiam.identica.provider.capability.bootstrap.ProviderCapabilityBootstrap; +import me.whereareiam.identica.provider.capability.bootstrap.ProviderCapabilityGlobalInstallContext; +import me.whereareiam.identica.provider.capability.bootstrap.ProviderCapabilityInitializationContext; +import me.whereareiam.identica.provider.capability.bootstrap.ProviderCapabilityLocalInstallContext; +import me.whereareiam.identica.provider.capability.contribution.ProviderCapabilityContribution; +import me.whereareiam.identica.type.provider.ProviderState; +import me.whereareiam.identica.type.provider.capability.ProviderCapability; +import me.whereareiam.identica.type.provider.capability.ProviderCapabilityScope; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.nio.file.Path; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.*; + +class DefaultProviderCapabilityCoordinatorTest { + private static final ProviderCapability SAMPLE_CAPABILITY = ProviderCapability.of("sample"); + private static final AtomicInteger GLOBAL_INITIALIZE_COUNT = new AtomicInteger(); + + @Test + void installsGlobalRuntimeOnceAndResolvesLocalModules(@TempDir Path tempDir) { + GLOBAL_INITIALIZE_COUNT.set(0); + Injector injector = Guice.createInjector(new CapabilityTestModule(tempDir)); + ProviderCapabilityCoordinator coordinator = injector.getInstance(ProviderCapabilityCoordinator.class); + ProviderCapabilityRegistry registry = injector.getInstance(ProviderCapabilityRegistry.class); + + InternalProvider provider = provider("provider-a", tempDir); + List bootstraps = coordinator.resolveBootstraps( + provider.getDescriptor(), + List.of(SampleCapabilityBootstrap.INSTANCE) + ); + assertEquals(List.of(SAMPLE_CAPABILITY.getId()), provider.getDescriptor().getDeclaredCapabilityIds()); + assertTrue(provider.getDescriptor().hasCapability(SAMPLE_CAPABILITY)); + + coordinator.installGlobalCapabilities(provider, bootstraps); + + assertNotNull(registry.findInstallation(SAMPLE_CAPABILITY)); + assertEquals(1, GLOBAL_INITIALIZE_COUNT.get()); + + List localModules = coordinator.resolveLocalModules(provider, bootstraps); + Injector providerInjector = injector.createChildInjector(localModules); + ProviderCapabilityInstallation installation = registry.findInstallation(SAMPLE_CAPABILITY); + assertNotNull(installation); + assertNotNull(installation.getGlobalInjector()); + assertSame( + installation.getGlobalInjector().getInstance(SampleGlobalService.class), + providerInjector.getInstance(SampleGlobalService.class) + ); + assertEquals("provider-a", providerInjector.getInstance(SampleLocalService.class).providerId()); + + InternalProvider nextProvider = provider("provider-b", tempDir); + coordinator.installGlobalCapabilities(nextProvider, bootstraps); + assertEquals(1, registry.installations().size()); + assertEquals(1, GLOBAL_INITIALIZE_COUNT.get()); + } + + @Test + void rejectsMissingRequiredContribution() { + Injector injector = Guice.createInjector(new CapabilityTestModule(Path.of("build", "tmp", "capability-test"))); + ProviderCapabilityCoordinator coordinator = injector.getInstance(ProviderCapabilityCoordinator.class); + ProviderDescriptor descriptor = descriptor("provider-a"); + List bootstraps = coordinator.resolveBootstraps( + descriptor, + List.of(RequiredContributionCapabilityBootstrap.INSTANCE) + ); + + IllegalStateException exception = assertThrows(IllegalStateException.class, () -> + coordinator.validateCapabilityContributions(descriptor, bootstraps, Set.of())); + + assertTrue(exception.getMessage().contains("sample")); + } + + @Test + void resolvesContributionsFromProviderInjector() { + Injector injector = Guice.createInjector( + new CapabilityTestModule(Path.of("build", "tmp", "capability-test")), + new ContributionModule() + ); + ProviderCapabilityCoordinator coordinator = injector.getInstance(ProviderCapabilityCoordinator.class); + + Set contributions = coordinator.resolveCapabilityContributions(injector); + + assertEquals(1, contributions.size()); + assertEquals(SAMPLE_CAPABILITY, contributions.iterator().next().capability()); + } + + private static @NotNull InternalProvider provider(@NotNull String id, @NotNull Path workingPath) { + return InternalProvider.builder() + .descriptor(descriptor(id)) + .workingPath(workingPath) + .state(ProviderState.DISCOVERED) + .build(); + } + + private static @NotNull ProviderDescriptor descriptor(@NotNull String id) { + ProviderDescriptor descriptor = new ProviderDescriptor(); + descriptor.setId(id); + descriptor.setName(id); + descriptor.setVersion("1.0.0"); + descriptor.setMain("ignored.Main"); + descriptor.setSupportedPlatforms(List.of("ANY")); + return descriptor; + } + + private static final class CapabilityTestModule extends AbstractModule { + private final Path rootPath; + + private CapabilityTestModule(@NotNull Path rootPath) { + this.rootPath = rootPath; + } + + @Override + protected void configure() { + bind(ProviderCapabilityCoordinator.class).to(DefaultProviderCapabilityCoordinator.class).asEagerSingleton(); + bind(ProviderCapabilityRegistry.class).to(DefaultProviderCapabilityRegistry.class).asEagerSingleton(); + bind(SharedCapabilityClassLoaderFactory.class).toInstance(new SharedCapabilityClassLoaderFactory()); + } + + @Provides + @Named("capabilitiesPath") + Path provideCapabilitiesPath() { + return rootPath.resolve("providers").resolve("capabilities"); + } + } + + private static final class ContributionModule extends AbstractModule { + @Override + protected void configure() { + Multibinder.newSetBinder(binder(), ProviderCapabilityContribution.class) + .addBinding() + .to(SampleContribution.class); + } + } + + public interface SampleGlobalService { + @NotNull String value(); + } + + public interface SampleLocalService { + @NotNull String providerId(); + } + + private static final class SampleCapabilityBootstrap implements ProviderCapabilityBootstrap { + private static final SampleCapabilityBootstrap INSTANCE = new SampleCapabilityBootstrap(); + + @Override + public @NotNull ProviderCapabilityDeclaration declaration() { + return ProviderCapabilityDeclaration.builder() + .capability(SAMPLE_CAPABILITY) + .scopes(Set.of(ProviderCapabilityScope.GLOBAL, ProviderCapabilityScope.LOCAL)) + .build(); + } + + @Override + public @NotNull List globalModules(@NotNull ProviderCapabilityGlobalInstallContext context) { + return List.of(new SampleGlobalModule()); + } + + @Override + public void initialize(@NotNull ProviderCapabilityInitializationContext context) { + Injector globalInjector = context.getGlobalInjector(); + assertNotNull(globalInjector); + + GLOBAL_INITIALIZE_COUNT.incrementAndGet(); + } + + @Override + public @NotNull List localModules(@NotNull ProviderCapabilityLocalInstallContext context) { + return List.of(new SampleLocalModule(context.getProviderId())); + } + } + + private static final class RequiredContributionCapabilityBootstrap implements ProviderCapabilityBootstrap { + private static final RequiredContributionCapabilityBootstrap INSTANCE = new RequiredContributionCapabilityBootstrap(); + + @Override + public @NotNull ProviderCapabilityDeclaration declaration() { + return ProviderCapabilityDeclaration.builder() + .capability(SAMPLE_CAPABILITY) + .requiresContribution(true) + .build(); + } + } + + private static final class SampleGlobalModule extends AbstractModule { + @Override + protected void configure() { + bind(SampleGlobalService.class).toInstance(() -> "global"); + } + } + + private static final class SampleLocalModule extends AbstractModule { + private final String providerId; + + private SampleLocalModule(String providerId) { + this.providerId = providerId; + } + + @Override + protected void configure() { + bind(SampleLocalService.class).toInstance(() -> providerId); + } + } + + @Singleton + private static final class SampleContribution implements ProviderCapabilityContribution { + @Override + public @NotNull ProviderCapability capability() { + return SAMPLE_CAPABILITY; + } + } +} diff --git a/identica-common/src/test/java/me/whereareiam/identica/common/provider/library/ProviderLibraryInstallerTest.java b/identica-common/src/test/java/me/whereareiam/identica/common/provider/library/ProviderLibraryInstallerTest.java new file mode 100644 index 000000000..355a1ba8e --- /dev/null +++ b/identica-common/src/test/java/me/whereareiam/identica/common/provider/library/ProviderLibraryInstallerTest.java @@ -0,0 +1,126 @@ +package me.whereareiam.identica.common.provider.library; + +import me.whereareiam.identica.common.provider.classloader.SharedCapabilityClassLoaderFactory; +import me.whereareiam.identica.common.provider.dependency.ProviderDependencyLoggingAdapter; +import me.whereareiam.identica.model.provider.ProviderDescriptor; +import me.whereareiam.identica.model.provider.dependency.ProviderLibraries; +import me.whereareiam.identica.model.provider.dependency.ProviderLibrary; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.net.URLClassLoader; +import java.nio.file.Path; +import java.util.List; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.Mockito.mock; + +class ProviderLibraryInstallerTest { + @Test + void installsSharedCapabilityApisIntoSharedLoaderAndProviderRuntimeIntoProviderLoader(@TempDir Path tempDir) { + SharedCapabilityClassLoaderFactory sharedCapabilityClassLoaderFactory = new SharedCapabilityClassLoaderFactory(); + RecordingProviderLibraryInstaller installer = new RecordingProviderLibraryInstaller( + tempDir, + tempDir.resolve("capabilities"), + mock(ProviderDependencyLoggingAdapter.class), + sharedCapabilityClassLoaderFactory + ); + URLClassLoader providerClassLoader = new URLClassLoader(new java.net.URL[0], sharedCapabilityClassLoaderFactory.sharedClassLoader()); + + installer.installSharedCapabilityApis(libraries(shared("recognition-api"), shared("external-api"))); + Call sharedCall = installer.lastCall(); + installer.installProviderRuntime( + descriptor(), + libraries(local("recognition-runtime"), local("external-runtime")), + providerClassLoader + ); + Call providerCall = installer.lastCall(); + + assertEquals(tempDir.resolve("capabilities"), sharedCall.basePath()); + assertEquals(".libraries/shared", sharedCall.cacheNamespace()); + assertSame(sharedCapabilityClassLoaderFactory.sharedClassLoader(), sharedCall.classLoader()); + assertEquals(Set.of("recognition-api", "external-api"), artifactIds(sharedCall.libraries())); + + assertEquals(tempDir, providerCall.basePath()); + assertEquals(".libraries/test-provider", providerCall.cacheNamespace()); + assertSame(providerClassLoader, providerCall.classLoader()); + assertEquals(Set.of("recognition-runtime", "external-runtime"), artifactIds(providerCall.libraries())); + } + + private static @NotNull ProviderDescriptor descriptor() { + ProviderDescriptor descriptor = new ProviderDescriptor(); + descriptor.setId("test-provider"); + descriptor.setName("Test Provider"); + descriptor.setVersion("1.0.0"); + descriptor.setMain("example.Provider"); + descriptor.setSupportedPlatforms(List.of("ANY")); + return descriptor; + } + + private static @NotNull ProviderLibraries libraries(@NotNull ProviderLibrary... libraries) { + ProviderLibraries providerLibraries = new ProviderLibraries(); + providerLibraries.setLibraries(List.of(libraries)); + return providerLibraries; + } + + private static @NotNull Set artifactIds(@NotNull ProviderLibraries libraries) { + return libraries.getLibraries().stream() + .map(ProviderLibrary::getArtifactId) + .collect(java.util.stream.Collectors.toSet()); + } + + private static @NotNull ProviderLibrary shared(@NotNull String artifactId) { + return ProviderLibrary.builder() + .groupId("me.whereareiam.identica.capability") + .artifactId(artifactId) + .version("1.0.0") + .loader("shared") + .build(); + } + + private static @NotNull ProviderLibrary local(@NotNull String artifactId) { + return ProviderLibrary.builder() + .groupId("me.whereareiam.identica.capability") + .artifactId(artifactId) + .version("1.0.0") + .build(); + } + + private static final class RecordingProviderLibraryInstaller extends ProviderLibraryInstaller { + private Call lastCall; + + private RecordingProviderLibraryInstaller( + @NotNull Path providersPath, + @NotNull Path capabilitiesPath, + @NotNull ProviderDependencyLoggingAdapter loggingHelper, + @NotNull SharedCapabilityClassLoaderFactory sharedCapabilityClassLoaderFactory + ) { + super(providersPath, capabilitiesPath, loggingHelper, sharedCapabilityClassLoaderFactory); + } + + @Override + protected void install( + @NotNull Path basePath, + @NotNull String cacheNamespace, + @NotNull ProviderLibraries libraries, + @NotNull URLClassLoader classLoader + ) { + lastCall = new Call(basePath, cacheNamespace, libraries, classLoader); + } + + private @NotNull Call lastCall() { + return lastCall; + } + } + + private record Call( + @NotNull Path basePath, + @NotNull String cacheNamespace, + @NotNull ProviderLibraries libraries, + @NotNull URLClassLoader classLoader + ) { + } +} diff --git a/identica-common/src/test/java/me/whereareiam/identica/common/provider/library/ProviderLibraryPlannerTest.java b/identica-common/src/test/java/me/whereareiam/identica/common/provider/library/ProviderLibraryPlannerTest.java new file mode 100644 index 000000000..476b61c7a --- /dev/null +++ b/identica-common/src/test/java/me/whereareiam/identica/common/provider/library/ProviderLibraryPlannerTest.java @@ -0,0 +1,86 @@ +package me.whereareiam.identica.common.provider.library; + +import me.whereareiam.identica.model.provider.dependency.ProviderLibraries; +import me.whereareiam.identica.model.provider.dependency.ProviderLibrary; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ProviderLibraryPlannerTest { + @Test + void plansSharedAndProviderLibrariesSeparatelyAndPreservesRepositories() { + ProviderLibraries libraries = new ProviderLibraries(); + libraries.setRepositories(List.of("https://repo.example/releases")); + libraries.setLibraries(Arrays.asList( + shared("recognition-api", "1.0.0"), + local("recognition-runtime", "1.0.0"), + null, + shared("external-api", "2.0.0"), + local("external-runtime", "2.0.0") + )); + + ProviderLibraryPlanner.ProviderLibraryPlan plan = new ProviderLibraryPlanner(new SharedLibraryConflictTracker()) + .plan(libraries); + + assertEquals( + Set.of("recognition-api", "external-api"), + artifactIds(plan.sharedCapabilityApis()) + ); + assertEquals( + Set.of("recognition-runtime", "external-runtime"), + artifactIds(plan.providerRuntimeLibraries()) + ); + assertEquals(List.of("https://repo.example/releases"), plan.sharedCapabilityApis().getRepositories()); + assertEquals(List.of("https://repo.example/releases"), plan.providerRuntimeLibraries().getRepositories()); + } + + @Test + void deduplicatesMatchingSharedLibrariesButKeepsProviderRuntimeEntries() { + ProviderLibraries libraries = new ProviderLibraries(); + libraries.setLibraries(List.of( + shared("recognition-api", "1.0.0"), + shared("recognition-api", "1.0.0"), + local("recognition-runtime", "1.0.0") + )); + + ProviderLibraryPlanner.ProviderLibraryPlan plan = new ProviderLibraryPlanner(new SharedLibraryConflictTracker()) + .plan(libraries); + + assertEquals(List.of("recognition-api"), plan.sharedCapabilityApis().getLibraries().stream() + .map(ProviderLibrary::getArtifactId) + .toList()); + assertEquals(List.of("recognition-runtime"), plan.providerRuntimeLibraries().getLibraries().stream() + .map(ProviderLibrary::getArtifactId) + .toList()); + } + + private static @NotNull Set artifactIds(@NotNull ProviderLibraries libraries) { + return libraries.getLibraries().stream() + .map(ProviderLibrary::getArtifactId) + .collect(java.util.stream.Collectors.toSet()); + } + + private static @NotNull ProviderLibrary shared(@NotNull String artifactId, @NotNull String version) { + return ProviderLibrary.builder() + .groupId("me.whereareiam.identica.capability") + .artifactId(artifactId) + .version(version) + .resolveTransitiveDependencies(false) + .loader("shared") + .build(); + } + + private static @NotNull ProviderLibrary local(@NotNull String artifactId, @NotNull String version) { + return ProviderLibrary.builder() + .groupId("me.whereareiam.identica.capability") + .artifactId(artifactId) + .version(version) + .resolveTransitiveDependencies(false) + .build(); + } +} diff --git a/identica-common/src/test/java/me/whereareiam/identica/common/provider/library/SharedLibraryConflictTrackerTest.java b/identica-common/src/test/java/me/whereareiam/identica/common/provider/library/SharedLibraryConflictTrackerTest.java new file mode 100644 index 000000000..bcc9fc0c8 --- /dev/null +++ b/identica-common/src/test/java/me/whereareiam/identica/common/provider/library/SharedLibraryConflictTrackerTest.java @@ -0,0 +1,37 @@ +package me.whereareiam.identica.common.provider.library; + +import me.whereareiam.identica.model.provider.dependency.ProviderLibrary; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class SharedLibraryConflictTrackerTest { + @Test + void acceptsMatchingCoordinates() { + SharedLibraryConflictTracker registry = new SharedLibraryConflictTracker(); + + assertDoesNotThrow(() -> { + registry.register(library("recognition", "1.0.0")); + registry.register(library("recognition", "1.0.0")); + }); + } + + @Test + void rejectsConflictingVersions() { + SharedLibraryConflictTracker registry = new SharedLibraryConflictTracker(); + registry.register(library("recognition", "1.0.0")); + + assertThrows(IllegalStateException.class, () -> + registry.register(library("recognition", "2.0.0"))); + } + + private static ProviderLibrary library(String artifactId, String version) { + return ProviderLibrary.builder() + .groupId("me.whereareiam.identica.capability") + .artifactId(artifactId) + .version(version) + .resolveTransitiveDependencies(false) + .build(); + } +} diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/DefaultConnectionCoordinator.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/DefaultConnectionCoordinator.java index c60a79677..83615bfe9 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/DefaultConnectionCoordinator.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/DefaultConnectionCoordinator.java @@ -4,8 +4,8 @@ import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; import me.whereareiam.identica.ConnectionCoordinator; -import me.whereareiam.identica.engine.pipeline.completion.CompletionPipeline; -import me.whereareiam.identica.engine.pipeline.prepare.PreparePipeline; +import me.whereareiam.identica.engine.pipeline.completion.runtime.CompletionPipeline; +import me.whereareiam.identica.engine.pipeline.prepare.runtime.PreparePipeline; import me.whereareiam.identica.engine.pipeline.scenario.ScenarioPipeline; import me.whereareiam.identica.identity.actor.Identity; import me.whereareiam.identica.model.auth.ConnectionDecision; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/EngineConfiguration.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/EngineConfiguration.java index 9063ad11f..b8054c9cf 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/EngineConfiguration.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/EngineConfiguration.java @@ -5,20 +5,21 @@ import me.whereareiam.identica.ConnectionCoordinator; import me.whereareiam.identica.engine.pipeline.DefaultPipelineExtensionRegistry; import me.whereareiam.identica.engine.pipeline.DefaultPipelineStateStore; -import me.whereareiam.identica.engine.pipeline.PendingPipelineKickCoordinator; -import me.whereareiam.identica.engine.pipeline.PendingScenarioAccountLifecycle; -import me.whereareiam.identica.engine.pipeline.completion.CompletionPendingLifecycle; -import me.whereareiam.identica.engine.pipeline.completion.CompletionPipeline; -import me.whereareiam.identica.engine.pipeline.completion.CompletionPipelineRegistry; -import me.whereareiam.identica.engine.pipeline.completion.DefaultCompletionExtensionRegistry; +import me.whereareiam.identica.engine.pipeline.completion.extension.DefaultCompletionExtensionRegistry; +import me.whereareiam.identica.engine.pipeline.completion.lifecycle.CompletionPendingLifecycle; +import me.whereareiam.identica.engine.pipeline.completion.registry.CompletionPipelineRegistry; +import me.whereareiam.identica.engine.pipeline.completion.runtime.CompletionPipeline; +import me.whereareiam.identica.engine.pipeline.prepare.registry.PreparePipelineRegistry; +import me.whereareiam.identica.engine.pipeline.scenario.PendingPipelineKickCoordinator; +import me.whereareiam.identica.engine.pipeline.scenario.PendingScenarioAccountLifecycle; import me.whereareiam.identica.engine.pipeline.scenario.ScenarioRegistry; -import me.whereareiam.identica.engine.pipeline.scenario.authentication.AuthenticationPipelineRegistry; -import me.whereareiam.identica.engine.pipeline.scenario.authentication.DefaultAuthenticationStageRegistry; -import me.whereareiam.identica.engine.pipeline.scenario.migration.DefaultMigrationStageRegistry; -import me.whereareiam.identica.engine.pipeline.scenario.migration.MigrationPipelineRegistry; -import me.whereareiam.identica.engine.pipeline.scenario.registration.DefaultRegistrationStageRegistry; -import me.whereareiam.identica.engine.pipeline.scenario.registration.RegistrationPipelineRegistry; -import me.whereareiam.identica.engine.pipeline.scenario.base.journey.rule.DefaultJourneyRuleRegistry; +import me.whereareiam.identica.engine.pipeline.scenario.shared.group.journey.rule.DefaultJourneyRuleRegistry; +import me.whereareiam.identica.engine.pipeline.scenario.type.authentication.AuthenticationPipelineRegistry; +import me.whereareiam.identica.engine.pipeline.scenario.type.authentication.DefaultAuthenticationStageRegistry; +import me.whereareiam.identica.engine.pipeline.scenario.type.migration.DefaultMigrationStageRegistry; +import me.whereareiam.identica.engine.pipeline.scenario.type.migration.MigrationPipelineRegistry; +import me.whereareiam.identica.engine.pipeline.scenario.type.registration.DefaultRegistrationStageRegistry; +import me.whereareiam.identica.engine.pipeline.scenario.type.registration.RegistrationPipelineRegistry; import me.whereareiam.identica.pipeline.PipelineRegistry; import me.whereareiam.identica.pipeline.completion.extension.CompletionExtensionRegistry; import me.whereareiam.identica.pipeline.extension.PipelineExtensionRegistry; @@ -36,6 +37,9 @@ protected void configure() { bind(PipelineRegistry.class).annotatedWith(Names.named("authenticationPipelineRegistry")) .to(AuthenticationPipelineRegistry.class) .asEagerSingleton(); + bind(PipelineRegistry.class).annotatedWith(Names.named("preparePipelineRegistry")) + .to(PreparePipelineRegistry.class) + .asEagerSingleton(); bind(PipelineRegistry.class).annotatedWith(Names.named("registrationPipelineRegistry")) .to(RegistrationPipelineRegistry.class) .asEagerSingleton(); diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registry/AbstractPipelineGroupRegistry.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/AbstractPipelineGroupRegistry.java similarity index 94% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registry/AbstractPipelineGroupRegistry.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/AbstractPipelineGroupRegistry.java index d8c6bb957..a22782132 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registry/AbstractPipelineGroupRegistry.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/AbstractPipelineGroupRegistry.java @@ -1,19 +1,14 @@ -package me.whereareiam.identica.engine.pipeline.scenario.registry; +package me.whereareiam.identica.engine.pipeline; -import me.whereareiam.identica.model.pipeline.state.PipelineState; +import me.whereareiam.identica.logging.Logger; +import me.whereareiam.identica.model.pipeline.phase.PhasePlacement; import me.whereareiam.identica.pipeline.PipelineGroup; -import me.whereareiam.identica.pipeline.PipelineRegistry; import me.whereareiam.identica.pipeline.PipelinePhase; -import me.whereareiam.identica.model.pipeline.phase.PhasePlacement; -import me.whereareiam.identica.logging.Logger; +import me.whereareiam.identica.pipeline.PipelineRegistry; +import me.whereareiam.identica.pipeline.state.PipelineState; import org.jetbrains.annotations.NotNull; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Locale; -import java.util.Map; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/DefaultPipelineExtensionRegistry.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/DefaultPipelineExtensionRegistry.java index e967c7068..18a4f934f 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/DefaultPipelineExtensionRegistry.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/DefaultPipelineExtensionRegistry.java @@ -4,38 +4,33 @@ import com.google.inject.Singleton; import com.google.inject.name.Named; import me.whereareiam.identica.logging.Logger; +import me.whereareiam.identica.model.pipeline.journey.stage.JourneyStage; +import me.whereareiam.identica.model.pipeline.journey.stage.step.JourneyStep; +import me.whereareiam.identica.model.pipeline.phase.PhasePlacement; +import me.whereareiam.identica.pipeline.PipelineGroup; +import me.whereareiam.identica.pipeline.PipelinePhase; import me.whereareiam.identica.pipeline.PipelineRegistry; import me.whereareiam.identica.pipeline.extension.PipelineExtension; import me.whereareiam.identica.pipeline.extension.PipelineExtensionBuilder; import me.whereareiam.identica.pipeline.extension.PipelineExtensionRegistry; -import me.whereareiam.identica.pipeline.PipelineGroup; -import me.whereareiam.identica.model.pipeline.phase.PhasePlacement; -import me.whereareiam.identica.pipeline.PipelinePhase; -import me.whereareiam.identica.pipeline.journey.registry.type.AuthenticationJourneyRegistry; import me.whereareiam.identica.pipeline.journey.registry.JourneyRegistry; +import me.whereareiam.identica.pipeline.journey.registry.type.AuthenticationJourneyRegistry; import me.whereareiam.identica.pipeline.journey.registry.type.MigrationJourneyRegistry; -import me.whereareiam.identica.model.pipeline.journey.stage.JourneyStage; -import me.whereareiam.identica.model.pipeline.journey.stage.step.JourneyStep; import me.whereareiam.identica.pipeline.journey.registry.type.RegistrationJourneyRegistry; import me.whereareiam.identica.pipeline.journey.step.Step; -import me.whereareiam.identica.type.pipeline.PipelineScope; import me.whereareiam.identica.type.pipeline.PipelineType; import me.whereareiam.identica.type.pipeline.journey.JourneyMode; import me.whereareiam.identica.type.pipeline.journey.StageType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.EnumSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; @Singleton public class DefaultPipelineExtensionRegistry implements PipelineExtensionRegistry { private final PipelineRegistry authenticationRegistry; + private final PipelineRegistry prepareRegistry; private final PipelineRegistry registrationRegistry; private final PipelineRegistry migrationRegistry; private final AuthenticationJourneyRegistry authenticationStageRegistry; @@ -46,6 +41,7 @@ public class DefaultPipelineExtensionRegistry implements PipelineExtensionRegist @Inject public DefaultPipelineExtensionRegistry( @Named("authenticationPipelineRegistry") PipelineRegistry authenticationRegistry, + @Named("preparePipelineRegistry") PipelineRegistry prepareRegistry, @Named("registrationPipelineRegistry") PipelineRegistry registrationRegistry, @Named("migrationPipelineRegistry") PipelineRegistry migrationRegistry, AuthenticationJourneyRegistry authenticationStageRegistry, @@ -53,6 +49,7 @@ public DefaultPipelineExtensionRegistry( MigrationJourneyRegistry migrationStageRegistry ) { this.authenticationRegistry = authenticationRegistry; + this.prepareRegistry = prepareRegistry; this.registrationRegistry = registrationRegistry; this.migrationRegistry = migrationRegistry; this.authenticationStageRegistry = authenticationStageRegistry; @@ -101,47 +98,63 @@ public synchronized boolean unregister(@NotNull String extensionId) { return List.copyOf(snapshot); } - private @NotNull PipelineRegistry resolveRegistry(@NotNull PipelineScope scope) { - return switch (scope) { + private @NotNull PipelineRegistry resolveScenarioRegistry(@NotNull PipelineType pipelineType) { + return switch (pipelineType) { case AUTHENTICATION -> authenticationRegistry; case REGISTRATION -> registrationRegistry; case MIGRATION -> migrationRegistry; }; } - private @NotNull JourneyRegistry resolveJourneyRegistry(@NotNull PipelineScope scope) { - return scope == PipelineScope.REGISTRATION - ? registrationStageRegistry - : scope == PipelineScope.MIGRATION - ? migrationStageRegistry - : authenticationStageRegistry; + private @NotNull JourneyRegistry resolveJourneyRegistry(@NotNull PipelineType pipelineType) { + return switch (pipelineType) { + case AUTHENTICATION -> authenticationStageRegistry; + case REGISTRATION -> registrationStageRegistry; + case MIGRATION -> migrationStageRegistry; + }; } private final class RegistrationJournal implements PipelineExtensionBuilder { private final List rollbackActions = new ArrayList<>(); @Override - public void registerGroup(@NotNull PipelineScope scope, @NotNull PipelineGroup group) { - PipelineRegistry registry = resolveRegistry(scope); + public void registerPrepareGroup(@NotNull PipelineGroup group) { + prepareRegistry.register(group); + rollbackActions.add(() -> prepareRegistry.unregister(group.id())); + } + + @Override + public void registerPreparePhase( + @NotNull String groupId, + @NotNull PipelinePhase phase, + @NotNull PhasePlacement placement + ) { + prepareRegistry.registerPhase(groupId, phase, placement); + rollbackActions.add(() -> prepareRegistry.unregisterPhase(groupId, phase.id())); + } + + @Override + public void registerScenarioGroup(@NotNull PipelineType pipelineType, @NotNull PipelineGroup group) { + PipelineRegistry registry = resolveScenarioRegistry(pipelineType); registry.register(group); rollbackActions.add(() -> registry.unregister(group.id())); } @Override - public void registerPhase( - @NotNull PipelineScope scope, + public void registerScenarioPhase( + @NotNull PipelineType pipelineType, @NotNull String groupId, @NotNull PipelinePhase phase, @NotNull PhasePlacement placement ) { - PipelineRegistry registry = resolveRegistry(scope); + PipelineRegistry registry = resolveScenarioRegistry(pipelineType); registry.registerPhase(groupId, phase, placement); rollbackActions.add(() -> registry.unregisterPhase(groupId, phase.id())); } @Override - public void registerStage(@NotNull PipelineScope scope, @NotNull JourneyStage stage) { - JourneyRegistry registry = resolveJourneyRegistry(scope); + public void registerStage(@NotNull PipelineType pipelineType, @NotNull JourneyStage stage) { + JourneyRegistry registry = resolveJourneyRegistry(pipelineType); registry.registerStage(stage); rollbackActions.add(() -> registry.unregisterStage(stage.getId())); } @@ -155,11 +168,7 @@ public void registerStep(@NotNull JourneyStep step) { } for (PipelineType pipelineType : targetTypes) { - JourneyRegistry registry = pipelineType == PipelineType.REGISTRATION - ? registrationStageRegistry - : pipelineType == PipelineType.MIGRATION - ? migrationStageRegistry - : authenticationStageRegistry; + JourneyRegistry registry = resolveJourneyRegistry(pipelineType); registry.registerStep(step.toBuilder() .scenarios(EnumSet.of(pipelineType)) .build()); @@ -171,27 +180,25 @@ public void registerStep(@NotNull JourneyStep step) { @Override public void registerStep( - @NotNull PipelineScope scope, + @NotNull PipelineType pipelineType, @Nullable String providerId, @NotNull StageType stageType, - @NotNull PipelineType pipelineType, @NotNull Step step ) { - JourneyRegistry registry = resolveJourneyRegistry(scope); + JourneyRegistry registry = resolveJourneyRegistry(pipelineType); registry.registerStep(providerId, stageType, step.order(), pipelineType, step); recordStepRollback(registry, stageType.id(), providerId, pipelineType, step); } @Override public void registerStep( - @NotNull PipelineScope scope, + @NotNull PipelineType pipelineType, @Nullable String providerId, @NotNull StageType stageType, - @NotNull PipelineType pipelineType, @NotNull JourneyMode journeyMode, @NotNull Step step ) { - JourneyRegistry registry = resolveJourneyRegistry(scope); + JourneyRegistry registry = resolveJourneyRegistry(pipelineType); registry.registerStep(providerId, stageType, step.order(), pipelineType, journeyMode, step); recordStepRollback(registry, stageType.id(), providerId, pipelineType, step); } diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/DefaultPipelineStateStore.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/DefaultPipelineStateStore.java index 829bebe2b..6bad3a54b 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/DefaultPipelineStateStore.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/DefaultPipelineStateStore.java @@ -9,10 +9,10 @@ import me.whereareiam.identica.event.pipeline.state.PipelineStateSavedEvent; import me.whereareiam.identica.logging.Logger; import me.whereareiam.identica.model.config.Replication; -import me.whereareiam.identica.model.pipeline.state.PipelineState; -import me.whereareiam.identica.model.pipeline.state.PipelineStateReference; import me.whereareiam.identica.model.replication.ReplicationType; import me.whereareiam.identica.pipeline.ScenarioContext; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.PipelineStateReference; import me.whereareiam.identica.pipeline.state.PipelineStateStore; import me.whereareiam.identica.replication.ReplicationSystem; import me.whereareiam.identica.replication.cache.ReplicatedCache; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/PipelineExecutor.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/PipelineExecutor.java index 331742fd3..d793b2e69 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/PipelineExecutor.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/PipelineExecutor.java @@ -1,14 +1,14 @@ package me.whereareiam.identica.engine.pipeline; import com.google.inject.Singleton; +import me.whereareiam.identica.model.pipeline.GroupOutcome; import me.whereareiam.identica.model.pipeline.PipelineCursor; import me.whereareiam.identica.model.pipeline.PipelineResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; -import me.whereareiam.identica.pipeline.PipelineRegistry; -import me.whereareiam.identica.model.pipeline.GroupOutcome; -import me.whereareiam.identica.pipeline.PipelineGroup; import me.whereareiam.identica.model.pipeline.phase.PhaseResult; +import me.whereareiam.identica.pipeline.PipelineGroup; import me.whereareiam.identica.pipeline.PipelinePhase; +import me.whereareiam.identica.pipeline.PipelineRegistry; +import me.whereareiam.identica.pipeline.state.PipelineState; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/DefaultCompletionExtensionRegistry.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/extension/DefaultCompletionExtensionRegistry.java similarity index 98% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/DefaultCompletionExtensionRegistry.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/extension/DefaultCompletionExtensionRegistry.java index 726c22f46..52eaefdba 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/DefaultCompletionExtensionRegistry.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/extension/DefaultCompletionExtensionRegistry.java @@ -1,12 +1,11 @@ -package me.whereareiam.identica.engine.pipeline.completion; +package me.whereareiam.identica.engine.pipeline.completion.extension; -import com.google.inject.Inject; import com.google.inject.Singleton; +import me.whereareiam.identica.logging.Logger; import me.whereareiam.identica.pipeline.completion.extension.CompletionExtension; import me.whereareiam.identica.pipeline.completion.extension.CompletionExtensionBuilder; import me.whereareiam.identica.pipeline.completion.extension.CompletionExtensionRegistry; import me.whereareiam.identica.pipeline.completion.step.CompletionStep; -import me.whereareiam.identica.logging.Logger; import me.whereareiam.identica.type.pipeline.PipelineType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/group/context/CompletionContextGroup.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/group/context/CompletionContextGroup.java index 3cca5057a..d55a704e8 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/group/context/CompletionContextGroup.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/group/context/CompletionContextGroup.java @@ -1,11 +1,11 @@ package me.whereareiam.identica.engine.pipeline.completion.group.context; import com.google.inject.Singleton; -import me.whereareiam.identica.engine.pipeline.completion.CompletionPipelineState; import me.whereareiam.identica.model.pipeline.GroupOutcome; import me.whereareiam.identica.model.pipeline.PipelineResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.pipeline.PipelineGroup; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.completion.CompletionPipelineState; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/group/context/phase/BuildCompletionContextPhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/group/context/phase/BuildCompletionContextPhase.java index 1a7116c92..f2f5e019d 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/group/context/phase/BuildCompletionContextPhase.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/group/context/phase/BuildCompletionContextPhase.java @@ -1,12 +1,12 @@ package me.whereareiam.identica.engine.pipeline.completion.group.context.phase; import com.google.inject.Singleton; -import me.whereareiam.identica.engine.pipeline.completion.CompletionPipelineState; import me.whereareiam.identica.model.Session; import me.whereareiam.identica.model.pipeline.completion.CompletionContext; import me.whereareiam.identica.model.pipeline.phase.PhaseResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.pipeline.PipelinePhase; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.completion.CompletionPipelineState; import org.jetbrains.annotations.NotNull; import java.util.concurrent.CompletableFuture; @@ -42,7 +42,6 @@ public int order() { .pipelineType(state.getPipelineType()) .session(session) .provider(state.getProvider()) - .authenticationRecognized(state.isAuthenticationRecognized()) .build(); state.setContext(context); return CompletableFuture.completedFuture(PhaseResult.pass(state)); diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/group/context/phase/ResolveCompletionProviderPhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/group/context/phase/ResolveCompletionProviderPhase.java index ff19f7b10..cb5a5f4c5 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/group/context/phase/ResolveCompletionProviderPhase.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/group/context/phase/ResolveCompletionProviderPhase.java @@ -3,12 +3,12 @@ import com.google.inject.Inject; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.engine.pipeline.completion.CompletionPipelineState; import me.whereareiam.identica.model.Session; import me.whereareiam.identica.model.pipeline.phase.PhaseResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.model.provider.InternalProvider; import me.whereareiam.identica.pipeline.PipelinePhase; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.completion.CompletionPipelineState; import me.whereareiam.identica.provider.ProviderManager; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/group/context/phase/ResolveCompletionSessionPhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/group/context/phase/ResolveCompletionSessionPhase.java index 9ef38b2dd..9efea2543 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/group/context/phase/ResolveCompletionSessionPhase.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/group/context/phase/ResolveCompletionSessionPhase.java @@ -3,12 +3,12 @@ import com.google.inject.Inject; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.engine.pipeline.completion.CompletionPipelineState; import me.whereareiam.identica.identity.session.SessionService; import me.whereareiam.identica.model.Session; import me.whereareiam.identica.model.pipeline.phase.PhaseResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.pipeline.PipelinePhase; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.completion.CompletionPipelineState; import org.jetbrains.annotations.NotNull; import java.util.UUID; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/group/step/CompletionStepGroup.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/group/step/CompletionStepGroup.java index 2511b6fd4..801f8bd61 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/group/step/CompletionStepGroup.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/group/step/CompletionStepGroup.java @@ -1,11 +1,11 @@ package me.whereareiam.identica.engine.pipeline.completion.group.step; import com.google.inject.Singleton; -import me.whereareiam.identica.engine.pipeline.completion.CompletionPipelineState; import me.whereareiam.identica.model.pipeline.GroupOutcome; import me.whereareiam.identica.model.pipeline.PipelineResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.pipeline.PipelineGroup; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.completion.CompletionPipelineState; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/group/step/phase/ExecuteCompletionStepsPhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/group/step/phase/ExecuteCompletionStepsPhase.java index de26e9a54..ad9e4be48 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/group/step/phase/ExecuteCompletionStepsPhase.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/group/step/phase/ExecuteCompletionStepsPhase.java @@ -3,14 +3,14 @@ import com.google.inject.Inject; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.engine.pipeline.completion.CompletionPipelineState; import me.whereareiam.identica.logging.Logger; import me.whereareiam.identica.model.pipeline.completion.CompletionContext; import me.whereareiam.identica.model.pipeline.phase.PhaseResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.pipeline.PipelinePhase; import me.whereareiam.identica.pipeline.completion.extension.CompletionExtensionRegistry; import me.whereareiam.identica.pipeline.completion.step.CompletionStep; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.completion.CompletionPipelineState; import org.jetbrains.annotations.NotNull; import java.util.List; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/CompletionPendingLifecycle.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/lifecycle/CompletionPendingLifecycle.java similarity index 96% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/CompletionPendingLifecycle.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/lifecycle/CompletionPendingLifecycle.java index f0bb71a5d..6ccb86b58 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/CompletionPendingLifecycle.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/lifecycle/CompletionPendingLifecycle.java @@ -1,8 +1,9 @@ -package me.whereareiam.identica.engine.pipeline.completion; +package me.whereareiam.identica.engine.pipeline.completion.lifecycle; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; +import me.whereareiam.identica.engine.pipeline.completion.runtime.CompletionPipeline; import me.whereareiam.identica.event.EventListener; import me.whereareiam.identica.event.EventManager; import me.whereareiam.identica.event.base.IdenticEvent; @@ -69,7 +70,6 @@ public void onSessionOpened(@NotNull SessionOpenedEvent event) { .connectionUniqueId(event.getConnectionUniqueId()) .accountUniqueId(event.getSession().getUniqueId()) .pipelineType(event.getPipelineType()) - .authenticationRecognized(event.isAuthenticationRecognized()) .build()) .build()) .checkpoint(DeliveryCheckpoint.PLATFORM_READY_INITIAL) @@ -109,7 +109,6 @@ private void dispatchCompletion( .pipelineType(completion.getPipelineType()) .connectionUniqueId(completion.getConnectionUniqueId()) .accountUniqueId(completion.getAccountUniqueId()) - .authenticationRecognized(completion.isAuthenticationRecognized()) .build()); deliveryService.acknowledge(request.getId(), "completion-dispatched"); } diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/CompletionPipelineRegistry.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/registry/CompletionPipelineRegistry.java similarity index 92% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/CompletionPipelineRegistry.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/registry/CompletionPipelineRegistry.java index cd313126b..d346a206c 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/CompletionPipelineRegistry.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/registry/CompletionPipelineRegistry.java @@ -1,14 +1,14 @@ -package me.whereareiam.identica.engine.pipeline.completion; +package me.whereareiam.identica.engine.pipeline.completion.registry; import com.google.inject.Inject; import com.google.inject.Singleton; +import me.whereareiam.identica.engine.pipeline.AbstractPipelineGroupRegistry; import me.whereareiam.identica.engine.pipeline.completion.group.context.CompletionContextGroup; import me.whereareiam.identica.engine.pipeline.completion.group.context.phase.BuildCompletionContextPhase; import me.whereareiam.identica.engine.pipeline.completion.group.context.phase.ResolveCompletionProviderPhase; import me.whereareiam.identica.engine.pipeline.completion.group.context.phase.ResolveCompletionSessionPhase; import me.whereareiam.identica.engine.pipeline.completion.group.step.CompletionStepGroup; import me.whereareiam.identica.engine.pipeline.completion.group.step.phase.ExecuteCompletionStepsPhase; -import me.whereareiam.identica.engine.pipeline.scenario.registry.AbstractPipelineGroupRegistry; import me.whereareiam.identica.model.pipeline.phase.PhasePlacement; import me.whereareiam.identica.pipeline.PipelineRegistry; import org.jetbrains.annotations.NotNull; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/CompletionPipeline.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/runtime/CompletionPipeline.java similarity index 88% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/CompletionPipeline.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/runtime/CompletionPipeline.java index 6d3a7896a..13a55033a 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/CompletionPipeline.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/completion/runtime/CompletionPipeline.java @@ -1,16 +1,18 @@ -package me.whereareiam.identica.engine.pipeline.completion; +package me.whereareiam.identica.engine.pipeline.completion.runtime; import com.google.inject.Inject; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; import me.whereareiam.identica.engine.pipeline.PipelineExecutor; +import me.whereareiam.identica.engine.pipeline.completion.registry.CompletionPipelineRegistry; import me.whereareiam.identica.identity.actor.Identity; import me.whereareiam.identica.model.pipeline.GroupOutcome; import me.whereareiam.identica.model.pipeline.PipelineResult; import me.whereareiam.identica.model.pipeline.completion.CompletionPendingState; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.pipeline.PipelineGroup; import me.whereareiam.identica.pipeline.completion.CompletionPendingStore; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.completion.CompletionPipelineState; import org.jetbrains.annotations.NotNull; @Singleton @@ -32,7 +34,6 @@ public void complete(@NotNull Identity identity, @NotNull CompletionPendingState .identity(identity) .pendingState(pendingState) .pipelineType(pendingState.getPipelineType()) - .authenticationRecognized(pendingState.isAuthenticationRecognized()) .build(); PipelineState pipelineState = PipelineState.initial(); diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/AbstractPrepareGroup.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/AbstractPrepareGroup.java index 36c5e90b8..f25ba8321 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/AbstractPrepareGroup.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/AbstractPrepareGroup.java @@ -1,9 +1,10 @@ package me.whereareiam.identica.engine.pipeline.prepare.group; -import me.whereareiam.identica.model.pipeline.PipelineResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.model.pipeline.GroupOutcome; +import me.whereareiam.identica.model.pipeline.PipelineResult; import me.whereareiam.identica.pipeline.PipelineGroup; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.prepare.PrepareGroupState; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/context/ContextGroup.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/context/ContextGroup.java index a76fc6a32..968685c67 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/context/ContextGroup.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/context/ContextGroup.java @@ -2,7 +2,7 @@ import com.google.inject.Singleton; import me.whereareiam.identica.engine.pipeline.prepare.group.AbstractPrepareGroup; -import me.whereareiam.identica.engine.pipeline.prepare.group.PrepareGroupState; +import me.whereareiam.identica.pipeline.state.prepare.PrepareGroupState; import org.jetbrains.annotations.NotNull; @Singleton diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/context/phase/ResolveEntrypointPhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/context/phase/ResolveEntrypointPhase.java index 2e8a88d55..b91a0e51f 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/context/phase/ResolveEntrypointPhase.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/context/phase/ResolveEntrypointPhase.java @@ -3,15 +3,15 @@ import com.google.inject.Inject; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.engine.pipeline.prepare.ConnectionProviderContextResolver; -import me.whereareiam.identica.engine.pipeline.prepare.group.PrepareGroupState; +import me.whereareiam.identica.engine.pipeline.prepare.runtime.ConnectionProviderContextResolver; import me.whereareiam.identica.logging.Logger; -import me.whereareiam.identica.model.pipeline.state.PipelineState; +import me.whereareiam.identica.model.pipeline.phase.PhaseResult; import me.whereareiam.identica.model.pipeline.prepare.PrepareContextItem; import me.whereareiam.identica.model.provider.ProviderContext; -import me.whereareiam.identica.provider.ProviderOperations; -import me.whereareiam.identica.model.pipeline.phase.PhaseResult; import me.whereareiam.identica.pipeline.PipelinePhase; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.prepare.PrepareGroupState; +import me.whereareiam.identica.provider.ProviderOperations; import org.jetbrains.annotations.NotNull; import java.util.concurrent.CompletableFuture; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/context/phase/ResolvePendingMigrationContextPhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/context/phase/ResolvePendingMigrationContextPhase.java index bbb987466..e3a164499 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/context/phase/ResolvePendingMigrationContextPhase.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/context/phase/ResolvePendingMigrationContextPhase.java @@ -3,17 +3,17 @@ import com.google.inject.Inject; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.engine.pipeline.prepare.group.PrepareGroupState; import me.whereareiam.identica.logging.Logger; import me.whereareiam.identica.model.migration.MigrationContext; import me.whereareiam.identica.model.pipeline.migration.MigrationPendingState; import me.whereareiam.identica.model.pipeline.phase.PhaseResult; import me.whereareiam.identica.model.pipeline.prepare.PrepareContextItem; -import me.whereareiam.identica.model.pipeline.state.PipelineState; -import me.whereareiam.identica.model.pipeline.state.PipelineStateReference; import me.whereareiam.identica.model.provider.ProviderContext; import me.whereareiam.identica.pipeline.PipelinePhase; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.PipelineStateReference; import me.whereareiam.identica.pipeline.state.PipelineStateStore; +import me.whereareiam.identica.pipeline.state.prepare.PrepareGroupState; import me.whereareiam.identica.type.pipeline.PipelineType; import me.whereareiam.identica.type.provider.ProviderOrigin; import org.jetbrains.annotations.NotNull; @@ -82,13 +82,6 @@ public int order() { } private ProviderContext resolveProviderContext(@NotNull MigrationContext migration, @NotNull PrepareGroupState state) { - ProviderContext provider = migration.getProvider(); - if (provider != null) - return provider.toBuilder() - .providerUsername(state.getRequest().getIdentity().getUsername()) - .source(ProviderOrigin.MANUAL) - .build(); - return ProviderContext.of( migration.getTargetProviderId(), null, diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/context/phase/RestorePrepareStatePhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/context/phase/RestorePrepareStatePhase.java index 1f4f7c4c3..1db385853 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/context/phase/RestorePrepareStatePhase.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/context/phase/RestorePrepareStatePhase.java @@ -3,13 +3,13 @@ import com.google.inject.Inject; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.engine.pipeline.prepare.group.PrepareGroupState; import me.whereareiam.identica.model.pipeline.phase.PhaseResult; import me.whereareiam.identica.model.pipeline.prepare.PrepareContextItem; import me.whereareiam.identica.model.pipeline.prepare.decision.PrepareDecision; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.pipeline.PipelinePhase; import me.whereareiam.identica.pipeline.prepare.PrepareStateStore; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.prepare.PrepareGroupState; import org.jetbrains.annotations.NotNull; import java.util.concurrent.CompletableFuture; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/finalize/FinalizeGroup.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/finalize/FinalizeGroup.java index 5a26b347d..feba2071d 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/finalize/FinalizeGroup.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/finalize/FinalizeGroup.java @@ -2,9 +2,9 @@ import com.google.inject.Singleton; import me.whereareiam.identica.engine.pipeline.prepare.group.AbstractPrepareGroup; -import me.whereareiam.identica.engine.pipeline.prepare.group.PrepareGroupState; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.model.pipeline.GroupOutcome; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.prepare.PrepareGroupState; import org.jetbrains.annotations.NotNull; @Singleton diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/finalize/phase/StorePrepareDecisionPhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/finalize/phase/StorePrepareDecisionPhase.java index d04e894dd..28ce674e7 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/finalize/phase/StorePrepareDecisionPhase.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/finalize/phase/StorePrepareDecisionPhase.java @@ -3,15 +3,15 @@ import com.google.inject.Inject; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.engine.pipeline.prepare.group.PrepareGroupState; import me.whereareiam.identica.model.pipeline.phase.PhaseResult; import me.whereareiam.identica.model.pipeline.prepare.PrepareAccountCandidateItem; import me.whereareiam.identica.model.pipeline.prepare.PrepareContextItem; import me.whereareiam.identica.model.pipeline.prepare.decision.PrepareDecision; import me.whereareiam.identica.model.pipeline.prepare.decision.PrepareDecisionItem; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.pipeline.PipelinePhase; import me.whereareiam.identica.pipeline.prepare.PrepareStateStore; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.prepare.PrepareGroupState; import org.jetbrains.annotations.NotNull; import java.util.UUID; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/handshake/HandshakeGroup.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/handshake/HandshakeGroup.java index 34d624a11..32ced3a7f 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/handshake/HandshakeGroup.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/handshake/HandshakeGroup.java @@ -2,7 +2,7 @@ import com.google.inject.Singleton; import me.whereareiam.identica.engine.pipeline.prepare.group.AbstractPrepareGroup; -import me.whereareiam.identica.engine.pipeline.prepare.group.PrepareGroupState; +import me.whereareiam.identica.pipeline.state.prepare.PrepareGroupState; import org.jetbrains.annotations.NotNull; @Singleton diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/handshake/phase/EvaluateHandshakePhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/handshake/phase/EvaluateHandshakePhase.java index dda3e8c3d..41b0a7fa1 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/handshake/phase/EvaluateHandshakePhase.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/handshake/phase/EvaluateHandshakePhase.java @@ -3,27 +3,18 @@ import com.google.inject.Inject; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.engine.pipeline.prepare.group.PrepareGroupState; import me.whereareiam.identica.handshake.HandshakeStore; import me.whereareiam.identica.handshake.policy.HandshakePolicy; -import me.whereareiam.identica.handshake.policy.ProviderScopedHandshakePolicy; -import me.whereareiam.identica.identity.session.recognition.eligibility.RecognitionEligibilityService; import me.whereareiam.identica.logging.Logger; import me.whereareiam.identica.model.auth.handshake.HandshakeDecision; import me.whereareiam.identica.model.auth.handshake.HandshakeRequest; import me.whereareiam.identica.model.pipeline.phase.PhaseResult; import me.whereareiam.identica.model.pipeline.prepare.PrepareContextItem; import me.whereareiam.identica.model.pipeline.prepare.decision.PrepareDecisionItem; -import me.whereareiam.identica.model.pipeline.state.PipelineState; -import me.whereareiam.identica.model.provider.ProviderContext; -import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityContext; -import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityDecision; import me.whereareiam.identica.pipeline.PipelinePhase; -import me.whereareiam.identica.type.provider.ProviderOrigin; -import me.whereareiam.identica.type.session.recognition.RecognitionAttemptKind; -import me.whereareiam.identica.type.session.recognition.RecognitionTrigger; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.prepare.PrepareGroupState; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; @@ -32,7 +23,6 @@ @RequiredArgsConstructor(onConstructor_ = @Inject) public class EvaluateHandshakePhase implements PipelinePhase { private final HandshakeStore handshakeStore; - private final RecognitionEligibilityService recognitionEligibilityService; @Override public @NotNull String id() { @@ -75,62 +65,16 @@ public boolean supports(@NotNull PipelineState pipelineState, @NotNull PrepareGr context.getProvider() ); HandshakeDecision decision = HandshakeDecision.allow(); - String clientIp = state.getRequest().getIdentity().getIp(); - for (HandshakePolicy policy : handshakeStore.policies()) { - if (!shouldEvaluate(policy, context.getProvider(), clientIp, state.getRequest().getIdentity().getOrigin())) - continue; + Iterable policies = handshakeStore.policies(); + for (HandshakePolicy policy : policies) { decision = merge(decision, evaluatePolicy(policy, request)); - } + } context.applyHandshake(decision); pipelineState.putItem(context, 0L); return CompletableFuture.completedFuture(PhaseResult.pass(state)); } - private boolean shouldEvaluate( - @NotNull HandshakePolicy policy, - @Nullable ProviderContext provider, - @Nullable String clientIp, - @Nullable me.whereareiam.identica.identity.actor.ConnectionIdentity.Origin origin - ) { - if (!(policy instanceof ProviderScopedHandshakePolicy scoped)) return true; - if (provider == null || provider.getProviderId() == null || provider.getProviderId().isBlank()) return true; - if (!scoped.providerId().equalsIgnoreCase(provider.getProviderId())) - return false; - - RecognitionEligibilityDecision decision = recognitionEligibilityService.evaluate(RecognitionEligibilityContext.builder() - .providerId(scoped.providerId()) - .providerUsername(provider.getProviderUsername()) - .clientIp(clientIp) - .selectedProvider(provider) - .origin(origin) - .attemptKind(RecognitionAttemptKind.PROVIDER_HANDSHAKE_RECOGNITION) - .trigger(resolveTrigger(provider)) - .build()); - - if (!decision.isAllowed()) { - Logger.debug( - "Skipping provider-scoped handshake recognition provider=%s username=%s ip=%s rule=%s reason=%s", - scoped.providerId(), - provider.getProviderUsername(), - clientIp, - decision.getRuleId(), - decision.getReason() - ); - return false; - } - - return true; - } - - private @NotNull RecognitionTrigger resolveTrigger(@NotNull ProviderContext provider) { - ProviderOrigin source = provider.getSource(); - if (source == ProviderOrigin.ENTRYPOINT || source == ProviderOrigin.MANUAL) - return RecognitionTrigger.EXPLICIT_PROVIDER_SELECTION; - - return RecognitionTrigger.AUTOMATIC; - } - private @NotNull HandshakeDecision evaluatePolicy( @NotNull HandshakePolicy policy, @NotNull HandshakeRequest request diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/handshake/phase/FinalizeHandshakePhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/handshake/phase/FinalizeHandshakePhase.java index 21597628a..8de66dc6a 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/handshake/phase/FinalizeHandshakePhase.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/handshake/phase/FinalizeHandshakePhase.java @@ -4,17 +4,17 @@ import com.google.inject.Provider; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.engine.pipeline.prepare.group.PrepareGroupState; import me.whereareiam.identica.event.EventManager; import me.whereareiam.identica.event.handshake.HandshakeDecisionEvent; import me.whereareiam.identica.model.auth.handshake.HandshakeDecision; import me.whereareiam.identica.model.auth.handshake.HandshakeRequest; import me.whereareiam.identica.model.config.Messages; -import me.whereareiam.identica.model.pipeline.state.PipelineState; +import me.whereareiam.identica.model.pipeline.phase.PhaseResult; import me.whereareiam.identica.model.pipeline.prepare.PrepareContextItem; import me.whereareiam.identica.model.pipeline.prepare.decision.PrepareDecisionItem; -import me.whereareiam.identica.model.pipeline.phase.PhaseResult; import me.whereareiam.identica.pipeline.PipelinePhase; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.prepare.PrepareGroupState; import org.jetbrains.annotations.NotNull; import java.util.concurrent.CompletableFuture; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/policy/PolicyGroup.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/policy/PolicyGroup.java index 8c42f4f34..2960db838 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/policy/PolicyGroup.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/policy/PolicyGroup.java @@ -2,7 +2,7 @@ import com.google.inject.Singleton; import me.whereareiam.identica.engine.pipeline.prepare.group.AbstractPrepareGroup; -import me.whereareiam.identica.engine.pipeline.prepare.group.PrepareGroupState; +import me.whereareiam.identica.pipeline.state.prepare.PrepareGroupState; import org.jetbrains.annotations.NotNull; @Singleton diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/policy/phase/ApplyPreparePolicyPhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/policy/phase/ApplyPreparePolicyPhase.java deleted file mode 100644 index 80594dd11..000000000 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/policy/phase/ApplyPreparePolicyPhase.java +++ /dev/null @@ -1,130 +0,0 @@ -package me.whereareiam.identica.engine.pipeline.prepare.group.policy.phase; - -import com.google.inject.Inject; -import com.google.inject.Provider; -import com.google.inject.Singleton; -import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.engine.pipeline.prepare.group.PrepareGroupState; -import me.whereareiam.identica.event.EventManager; -import me.whereareiam.identica.event.account.AccountPrepareEvent; -import me.whereareiam.identica.logging.Logger; -import me.whereareiam.identica.model.config.Messages; -import me.whereareiam.identica.model.identity.AccountDecision; -import me.whereareiam.identica.model.pipeline.PipelineResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; -import me.whereareiam.identica.model.pipeline.prepare.PrepareAccountCandidateItem; -import me.whereareiam.identica.model.pipeline.prepare.PrepareContextItem; -import me.whereareiam.identica.model.pipeline.prepare.decision.PrepareDecisionItem; -import me.whereareiam.identica.model.pipeline.phase.PhaseResult; -import me.whereareiam.identica.pipeline.PipelinePhase; -import org.jetbrains.annotations.NotNull; - -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; - -@Singleton -@RequiredArgsConstructor(onConstructor_ = @Inject) -public class ApplyPreparePolicyPhase implements PipelinePhase { - private final EventManager eventManager; - private final Provider messagesProvider; - - @Override - public @NotNull String id() { - return "apply-prepare-policy"; - } - - @Override - public int order() { - return 100; - } - - @Override - public @NotNull Class stateType() { - return PrepareGroupState.class; - } - - @Override - public boolean supports(@NotNull PipelineState pipelineState, @NotNull PrepareGroupState state) { - if (pipelineState.item(PrepareDecisionItem.class).isPresent()) return false; - - PrepareAccountCandidateItem candidate = pipelineState.item(PrepareAccountCandidateItem.class).orElse(null); - return candidate != null - && candidate.getAccount() != null - && candidate.getLink() != null - && candidate.getProfile() != null; - } - - @Override - public @NotNull CompletionStage> execute( - @NotNull PipelineState pipelineState, - @NotNull PrepareGroupState state - ) { - PrepareContextItem context = pipelineState.item(PrepareContextItem.class).orElse(new PrepareContextItem()); - PrepareAccountCandidateItem candidate = pipelineState.item(PrepareAccountCandidateItem.class).orElse(null); - if (state.getRequest() == null || candidate == null - || candidate.getAccount() == null - || candidate.getLink() == null - || candidate.getProfile() == null) { - state.setResult(PipelineResult.failed(preparePolicyMissingMessage())); - return CompletableFuture.completedFuture(PhaseResult.pass(state)); - } - - String requestedUsername = state.getRequest().getIdentity().getUsername(); - var account = candidate.getAccount(); - var link = candidate.getLink(); - var profile = candidate.getProfile(); - - AccountPrepareEvent event = new AccountPrepareEvent( - requestedUsername, - context.getProvider(), - account, - link, - profile, - null - ); - eventManager.call(event); - - AccountDecision decision = event.getDecision(); - String effectiveUsername = event.getEffectiveUsername(); - if (effectiveUsername == null || effectiveUsername.isBlank()) - effectiveUsername = account.getUsername(); - if (effectiveUsername.isBlank()) - effectiveUsername = requestedUsername; - - candidate.setEffectiveUsername(effectiveUsername); - pipelineState.putItem(candidate, 0L); - if (decision != null && decision.isDenied()) { - Logger.info("Prepare denied by account review username=%s provider=%s", - requestedUsername, - context.getProvider() != null ? context.getProvider().getProviderId() : null); - pipelineState.putItem(PrepareDecisionItem.deny( - decision.getMessage(), - context, - candidate.getUniqueId(), - effectiveUsername - ), 0L); - return CompletableFuture.completedFuture(PhaseResult.pass(state)); - } - - Logger.debug("Prepare resolved username=%s effective=%s provider=%s account=%s", - requestedUsername, - effectiveUsername, - context.getProvider() != null ? context.getProvider().getProviderId() : null, - candidate.getUniqueId()); - pipelineState.putItem(PrepareDecisionItem.allow( - context, - candidate.getUniqueId(), - effectiveUsername - ), 0L); - - return CompletableFuture.completedFuture(PhaseResult.pass(state)); - } - - private @NotNull String preparePolicyMissingMessage() { - return String.join("\n", messagesProvider.get() - .getEngine() - .getPrepare() - .getErrors() - .getPreparePolicyMissing()); - } -} diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/profile/ProfileGroup.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/profile/ProfileGroup.java index 44c8128d0..cdb7e1519 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/profile/ProfileGroup.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/profile/ProfileGroup.java @@ -2,7 +2,7 @@ import com.google.inject.Singleton; import me.whereareiam.identica.engine.pipeline.prepare.group.AbstractPrepareGroup; -import me.whereareiam.identica.engine.pipeline.prepare.group.PrepareGroupState; +import me.whereareiam.identica.pipeline.state.prepare.PrepareGroupState; import org.jetbrains.annotations.NotNull; @Singleton diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/profile/phase/LoadPrepareAccountPhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/profile/phase/LoadPrepareAccountPhase.java index 8d8cf782f..e8850f09e 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/profile/phase/LoadPrepareAccountPhase.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/profile/phase/LoadPrepareAccountPhase.java @@ -6,7 +6,6 @@ import me.whereareiam.identica.database.AccountPersistenceService; import me.whereareiam.identica.database.provider.ProviderLinkPersistenceService; import me.whereareiam.identica.database.provider.ProviderProfilePersistenceService; -import me.whereareiam.identica.engine.pipeline.prepare.group.PrepareGroupState; import me.whereareiam.identica.identity.account.RegistrationAccountService; import me.whereareiam.identica.logging.Logger; import me.whereareiam.identica.model.auth.request.ProfileRequest; @@ -17,10 +16,10 @@ import me.whereareiam.identica.model.pipeline.prepare.PrepareAccountCandidateItem; import me.whereareiam.identica.model.pipeline.prepare.PrepareContextItem; import me.whereareiam.identica.model.pipeline.prepare.decision.PrepareDecisionItem; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.model.provider.ProviderContext; import me.whereareiam.identica.pipeline.PipelinePhase; -import me.whereareiam.identica.type.UsernameSource; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.prepare.PrepareGroupState; import org.jetbrains.annotations.NotNull; import java.util.Optional; @@ -115,7 +114,6 @@ public boolean supports(@NotNull PipelineState pipelineState, @NotNull PrepareGr : Account.builder() .uniqueId(accountUniqueId) .username(requestedUsername) - .source(UsernameSource.PROVIDER) .build(); AccountProviderProfile profile = providerProfilePersistenceService.findBySubject( provider.getProviderId(), diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/profile/phase/ResolvePendingMigrationAccountPhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/profile/phase/ResolvePendingMigrationAccountPhase.java index 4829c673f..0e2384e58 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/profile/phase/ResolvePendingMigrationAccountPhase.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/profile/phase/ResolvePendingMigrationAccountPhase.java @@ -4,31 +4,26 @@ import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; import me.whereareiam.identica.database.AccountPersistenceService; -import me.whereareiam.identica.database.provider.ProviderLinkPersistenceService; -import me.whereareiam.identica.database.provider.ProviderProfilePersistenceService; import me.whereareiam.identica.event.scenario.migration.MigrationResolvedEvent; -import me.whereareiam.identica.type.ScenarioResolution; -import me.whereareiam.identica.engine.pipeline.prepare.group.PrepareGroupState; import me.whereareiam.identica.logging.Logger; import me.whereareiam.identica.model.delivery.DeliveryPayload; import me.whereareiam.identica.model.delivery.DeliveryRequest; import me.whereareiam.identica.model.delivery.DeliveryTarget; import me.whereareiam.identica.model.identity.Account; -import me.whereareiam.identica.model.identity.provider.AccountProviderLink; -import me.whereareiam.identica.model.identity.provider.AccountProviderProfile; import me.whereareiam.identica.model.migration.MigrationContext; import me.whereareiam.identica.model.pipeline.migration.MigrationPendingState; import me.whereareiam.identica.model.pipeline.phase.PhaseResult; import me.whereareiam.identica.model.pipeline.prepare.PrepareAccountCandidateItem; import me.whereareiam.identica.model.pipeline.prepare.PrepareContextItem; import me.whereareiam.identica.model.pipeline.prepare.decision.PrepareDecisionItem; -import me.whereareiam.identica.model.pipeline.state.PipelineState; -import me.whereareiam.identica.model.pipeline.state.PipelineStateReference; import me.whereareiam.identica.model.provider.ProviderContext; import me.whereareiam.identica.pipeline.PipelinePhase; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.PipelineStateReference; import me.whereareiam.identica.pipeline.state.PipelineStateStore; +import me.whereareiam.identica.pipeline.state.prepare.PrepareGroupState; import me.whereareiam.identica.service.DeliveryService; -import me.whereareiam.identica.type.UsernameSource; +import me.whereareiam.identica.type.ScenarioResolution; import me.whereareiam.identica.type.messaging.DeliveryCheckpoint; import me.whereareiam.identica.type.messaging.DeliverySemantics; import me.whereareiam.identica.type.messaging.DeliverySource; @@ -36,7 +31,6 @@ import me.whereareiam.identica.util.EventUtil; import org.jetbrains.annotations.NotNull; -import java.util.Optional; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; @@ -46,8 +40,6 @@ public class ResolvePendingMigrationAccountPhase implements PipelinePhase { private final PipelineStateStore pipelineStateStore; private final AccountPersistenceService accountPersistenceService; - private final ProviderLinkPersistenceService providerLinkPersistenceService; - private final ProviderProfilePersistenceService providerProfilePersistenceService; private final DeliveryService deliveryService; @Override @@ -74,9 +66,7 @@ public boolean supports(@NotNull PipelineState pipelineState, @NotNull PrepareGr return context != null && context.getProvider() != null && context.getProvider().getProviderId() != null - && !context.getProvider().getProviderId().isBlank() - && context.getProvider().getProviderSubject() != null - && !context.getProvider().getProviderSubject().isBlank(); + && !context.getProvider().getProviderId().isBlank(); } @Override @@ -93,11 +83,8 @@ public boolean supports(@NotNull PipelineState pipelineState, @NotNull PrepareGr String connectionKey = request.getConnectionKey(); String providerId = provider.getProviderId(); - String providerSubject = provider.getProviderSubject(); String requestedUsername = request.getIdentity().getUsername(); - if (connectionKey == null || connectionKey.isBlank() - || providerId == null || providerId.isBlank() - || providerSubject == null || providerSubject.isBlank()) { + if (connectionKey == null || connectionKey.isBlank() || providerId == null || providerId.isBlank()) { return CompletableFuture.completedFuture(PhaseResult.pass(state)); } @@ -155,35 +142,20 @@ public boolean supports(@NotNull PipelineState pipelineState, @NotNull PrepareGr if (accountUniqueId == null) return CompletableFuture.completedFuture(PhaseResult.pass(state)); - Optional storedLink = providerLinkPersistenceService.findBySubject(providerId, providerSubject); - AccountProviderLink link = storedLink.orElseGet(() -> AccountProviderLink.builder() - .uniqueId(accountUniqueId) - .providerId(providerId) - .providerSubject(providerSubject) - .primaryLink(true) - .build()); - Account storedAccount = accountPersistenceService.findByUniqueId(accountUniqueId).orElse(null); Account account = storedAccount != null ? storedAccount.toBuilder().username(requestedUsername).build() : Account.builder() .uniqueId(accountUniqueId) .username(requestedUsername) - .source(UsernameSource.PROVIDER) .build(); - AccountProviderProfile profile = providerProfilePersistenceService.findBySubject(providerId, providerSubject) - .orElseGet(() -> AccountProviderProfile.builder() - .providerId(providerId) - .providerSubject(providerSubject) - .providerUsername(requestedUsername) - .build()); pipelineState.putItem(new PrepareAccountCandidateItem( accountUniqueId, null, account, - link, - profile + null, + null ), 0L); Logger.debug( "Prepare reusing pending migration account provider=%s identica=%s key=%s", diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/profile/phase/ResolvePreparedAccountPhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/profile/phase/ResolvePreparedAccountPhase.java index 19018edbc..e22b07e58 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/profile/phase/ResolvePreparedAccountPhase.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/profile/phase/ResolvePreparedAccountPhase.java @@ -6,7 +6,6 @@ import me.whereareiam.identica.database.AccountPersistenceService; import me.whereareiam.identica.database.provider.ProviderLinkPersistenceService; import me.whereareiam.identica.database.provider.ProviderProfilePersistenceService; -import me.whereareiam.identica.engine.pipeline.prepare.group.PrepareGroupState; import me.whereareiam.identica.model.identity.Account; import me.whereareiam.identica.model.identity.provider.AccountProviderLink; import me.whereareiam.identica.model.identity.provider.AccountProviderProfile; @@ -15,11 +14,11 @@ import me.whereareiam.identica.model.pipeline.prepare.PrepareContextItem; import me.whereareiam.identica.model.pipeline.prepare.decision.PrepareDecision; import me.whereareiam.identica.model.pipeline.prepare.decision.PrepareDecisionItem; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.model.provider.ProviderContext; import me.whereareiam.identica.pipeline.PipelinePhase; import me.whereareiam.identica.pipeline.prepare.PrepareStateStore; -import me.whereareiam.identica.type.UsernameSource; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.prepare.PrepareGroupState; import org.jetbrains.annotations.NotNull; import java.util.Optional; @@ -110,7 +109,6 @@ public boolean supports(@NotNull PipelineState pipelineState, @NotNull PrepareGr : Account.builder() .uniqueId(accountUniqueId) .username(requestedUsername) - .source(UsernameSource.PROVIDER) .build(); AccountProviderProfile profile = providerProfilePersistenceService.findBySubject( provider.getProviderId(), diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/profile/phase/ResolveProfilePhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/profile/phase/ResolveProfilePhase.java index 09e3d5507..1776f375c 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/profile/phase/ResolveProfilePhase.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/profile/phase/ResolveProfilePhase.java @@ -3,17 +3,17 @@ import com.google.inject.Inject; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.engine.pipeline.prepare.ConnectionProviderContextResolver; -import me.whereareiam.identica.engine.pipeline.prepare.group.PrepareGroupState; +import me.whereareiam.identica.engine.pipeline.prepare.runtime.ConnectionProviderContextResolver; import me.whereareiam.identica.logging.Logger; -import me.whereareiam.identica.model.pipeline.state.PipelineState; +import me.whereareiam.identica.model.pipeline.phase.PhaseResult; import me.whereareiam.identica.model.pipeline.prepare.PrepareContextItem; import me.whereareiam.identica.model.pipeline.prepare.decision.PrepareDecisionItem; import me.whereareiam.identica.model.provider.ProviderContext; +import me.whereareiam.identica.pipeline.PipelinePhase; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.prepare.PrepareGroupState; import me.whereareiam.identica.provider.ProviderOperations; import me.whereareiam.identica.provider.profile.ProfileResolveContext; -import me.whereareiam.identica.model.pipeline.phase.PhaseResult; -import me.whereareiam.identica.pipeline.PipelinePhase; import org.jetbrains.annotations.NotNull; import java.util.concurrent.CompletableFuture; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/PreparePipelineRegistry.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/registry/PreparePipelineRegistry.java similarity index 84% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/PreparePipelineRegistry.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/registry/PreparePipelineRegistry.java index 4f677c4db..ea07d4b80 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/PreparePipelineRegistry.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/registry/PreparePipelineRegistry.java @@ -1,7 +1,8 @@ -package me.whereareiam.identica.engine.pipeline.prepare; +package me.whereareiam.identica.engine.pipeline.prepare.registry; import com.google.inject.Inject; import com.google.inject.Singleton; +import me.whereareiam.identica.engine.pipeline.AbstractPipelineGroupRegistry; import me.whereareiam.identica.engine.pipeline.prepare.group.context.ContextGroup; import me.whereareiam.identica.engine.pipeline.prepare.group.context.phase.ResolveEntrypointPhase; import me.whereareiam.identica.engine.pipeline.prepare.group.context.phase.ResolvePendingMigrationContextPhase; @@ -12,14 +13,11 @@ import me.whereareiam.identica.engine.pipeline.prepare.group.handshake.phase.EvaluateHandshakePhase; import me.whereareiam.identica.engine.pipeline.prepare.group.handshake.phase.FinalizeHandshakePhase; import me.whereareiam.identica.engine.pipeline.prepare.group.policy.PolicyGroup; -import me.whereareiam.identica.engine.pipeline.prepare.group.policy.phase.ApplyPreparePolicyPhase; -import me.whereareiam.identica.engine.pipeline.prepare.group.policy.phase.ApplyProviderJoinRestrictionPhase; import me.whereareiam.identica.engine.pipeline.prepare.group.profile.ProfileGroup; import me.whereareiam.identica.engine.pipeline.prepare.group.profile.phase.LoadPrepareAccountPhase; import me.whereareiam.identica.engine.pipeline.prepare.group.profile.phase.ResolvePendingMigrationAccountPhase; import me.whereareiam.identica.engine.pipeline.prepare.group.profile.phase.ResolvePreparedAccountPhase; import me.whereareiam.identica.engine.pipeline.prepare.group.profile.phase.ResolveProfilePhase; -import me.whereareiam.identica.engine.pipeline.scenario.registry.AbstractPipelineGroupRegistry; import me.whereareiam.identica.model.pipeline.phase.PhasePlacement; import me.whereareiam.identica.pipeline.PipelineRegistry; import org.jetbrains.annotations.NotNull; @@ -42,8 +40,6 @@ public PreparePipelineRegistry( ResolvePendingMigrationAccountPhase resolvePendingMigrationAccountPhase, ResolvePreparedAccountPhase resolvePreparedAccountPhase, LoadPrepareAccountPhase loadPrepareAccountPhase, - ApplyProviderJoinRestrictionPhase applyProviderJoinRestrictionPhase, - ApplyPreparePolicyPhase applyPreparePolicyPhase, StorePrepareDecisionPhase storePrepareDecisionPhase ) { register(contextGroup); @@ -64,9 +60,6 @@ public PreparePipelineRegistry( registerPhase(profileGroup.id(), resolvePreparedAccountPhase, PhasePlacement.after(resolvePendingMigrationAccountPhase.id())); registerPhase(profileGroup.id(), loadPrepareAccountPhase, PhasePlacement.last()); - registerPhase(policyGroup.id(), applyProviderJoinRestrictionPhase, PhasePlacement.first()); - registerPhase(policyGroup.id(), applyPreparePolicyPhase, PhasePlacement.after(applyProviderJoinRestrictionPhase.id())); - registerPhase(finalizeGroup.id(), storePrepareDecisionPhase, PhasePlacement.first()); } diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/ConnectionProviderContextResolver.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/runtime/ConnectionProviderContextResolver.java similarity index 95% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/ConnectionProviderContextResolver.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/runtime/ConnectionProviderContextResolver.java index ab5a6b93f..a0ea883da 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/ConnectionProviderContextResolver.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/runtime/ConnectionProviderContextResolver.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.engine.pipeline.prepare; +package me.whereareiam.identica.engine.pipeline.prepare.runtime; import com.google.inject.Inject; import com.google.inject.Singleton; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/PreparePipeline.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/runtime/PreparePipeline.java similarity index 90% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/PreparePipeline.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/runtime/PreparePipeline.java index 1d0dddee7..0a8deb231 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/PreparePipeline.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/runtime/PreparePipeline.java @@ -1,18 +1,19 @@ -package me.whereareiam.identica.engine.pipeline.prepare; +package me.whereareiam.identica.engine.pipeline.prepare.runtime; import com.google.inject.Inject; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; import me.whereareiam.identica.engine.pipeline.PipelineExecutor; -import me.whereareiam.identica.engine.pipeline.prepare.group.PrepareGroupState; +import me.whereareiam.identica.engine.pipeline.prepare.registry.PreparePipelineRegistry; import me.whereareiam.identica.logging.Logger; -import me.whereareiam.identica.model.pipeline.state.PipelineState; +import me.whereareiam.identica.model.pipeline.GroupOutcome; import me.whereareiam.identica.model.pipeline.PipelineResult; -import me.whereareiam.identica.model.pipeline.prepare.decision.PrepareDecisionItem; -import me.whereareiam.identica.model.pipeline.prepare.decision.PrepareDecision; import me.whereareiam.identica.model.pipeline.prepare.PrepareRequest; -import me.whereareiam.identica.model.pipeline.GroupOutcome; +import me.whereareiam.identica.model.pipeline.prepare.decision.PrepareDecision; +import me.whereareiam.identica.model.pipeline.prepare.decision.PrepareDecisionItem; import me.whereareiam.identica.pipeline.PipelineGroup; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.prepare.PrepareGroupState; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registry/AbstractJourneyRegistry.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/AbstractJourneyRegistry.java similarity index 96% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registry/AbstractJourneyRegistry.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/AbstractJourneyRegistry.java index db36547a6..573c34cff 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registry/AbstractJourneyRegistry.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/AbstractJourneyRegistry.java @@ -1,24 +1,17 @@ -package me.whereareiam.identica.engine.pipeline.scenario.registry; +package me.whereareiam.identica.engine.pipeline.scenario; import me.whereareiam.identica.logging.Logger; -import me.whereareiam.identica.pipeline.ScenarioContext; import me.whereareiam.identica.model.pipeline.journey.JourneyPlan; -import me.whereareiam.identica.pipeline.journey.registry.JourneyRegistry; import me.whereareiam.identica.model.pipeline.journey.stage.JourneyStage; import me.whereareiam.identica.model.pipeline.journey.stage.step.JourneyStep; +import me.whereareiam.identica.pipeline.ScenarioContext; +import me.whereareiam.identica.pipeline.journey.registry.JourneyRegistry; import me.whereareiam.identica.type.pipeline.PipelineType; import me.whereareiam.identica.type.pipeline.journey.JourneyMode; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.EnumSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.concurrent.CopyOnWriteArrayList; public abstract class AbstractJourneyRegistry implements JourneyRegistry { diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/AbstractScenarioPipeline.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/AbstractScenarioPipeline.java index c9b66a0ba..15d6f4f6a 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/AbstractScenarioPipeline.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/AbstractScenarioPipeline.java @@ -4,7 +4,6 @@ import lombok.RequiredArgsConstructor; import me.whereareiam.identica.engine.pipeline.PipelineExecutor; import me.whereareiam.identica.event.pipeline.attempt.ScenarioContextBuiltEvent; -import me.whereareiam.identica.type.ScenarioResolution; import me.whereareiam.identica.identity.actor.ConnectionIdentity; import me.whereareiam.identica.logging.Logger; import me.whereareiam.identica.model.auth.ConnectionDecision; @@ -16,15 +15,16 @@ import me.whereareiam.identica.model.pipeline.GroupOutcome; import me.whereareiam.identica.model.pipeline.PipelineResult; import me.whereareiam.identica.model.pipeline.journey.JourneyStateItem; -import me.whereareiam.identica.model.pipeline.state.PipelineState; -import me.whereareiam.identica.model.pipeline.state.PipelineStateReference; import me.whereareiam.identica.pipeline.PipelineGroup; import me.whereareiam.identica.pipeline.PipelineRegistry; import me.whereareiam.identica.pipeline.ScenarioContext; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.PipelineStateReference; import me.whereareiam.identica.pipeline.state.PipelineStateStore; -import me.whereareiam.identica.type.pipeline.journey.JourneyMode; +import me.whereareiam.identica.type.ScenarioResolution; import me.whereareiam.identica.type.pipeline.PipelineStatus; import me.whereareiam.identica.type.pipeline.PipelineType; +import me.whereareiam.identica.type.pipeline.journey.JourneyMode; import me.whereareiam.identica.util.EventUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -145,7 +145,7 @@ public boolean matchesNewScenario(@Nullable ConnectionRequest request) { protected abstract @NotNull ScenarioContext mergeContext(@NotNull ScenarioContext base, @NotNull ResumeRequest request); - protected abstract boolean isPending(@NotNull PipelineState state); + public abstract boolean isPending(@NotNull PipelineState state); protected abstract void onStart(@NotNull PipelineState state, boolean resumed); diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/PendingPipelineKickCoordinator.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/PendingPipelineKickCoordinator.java similarity index 99% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/PendingPipelineKickCoordinator.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/PendingPipelineKickCoordinator.java index ac3df9350..3b8806bd0 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/PendingPipelineKickCoordinator.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/PendingPipelineKickCoordinator.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.engine.pipeline; +package me.whereareiam.identica.engine.pipeline.scenario; import com.google.inject.Inject; import com.google.inject.Provider; @@ -9,7 +9,6 @@ import me.whereareiam.identica.event.base.IdenticEvent; import me.whereareiam.identica.event.lifecycle.IdenticaShutdownEvent; import me.whereareiam.identica.event.scenario.ScenarioRequiredEvent; -import me.whereareiam.identica.type.ScenarioResolution; import me.whereareiam.identica.event.scenario.ScenarioResolvedEvent; import me.whereareiam.identica.event.scenario.authentication.AuthenticationResolvedEvent; import me.whereareiam.identica.event.scenario.migration.MigrationResolvedEvent; @@ -30,6 +29,7 @@ import me.whereareiam.identica.pipeline.ScenarioContext; import me.whereareiam.identica.service.DeliveryService; import me.whereareiam.identica.service.Scheduler; +import me.whereareiam.identica.type.ScenarioResolution; import me.whereareiam.identica.type.messaging.DeliveryCheckpoint; import me.whereareiam.identica.type.messaging.DeliverySemantics; import me.whereareiam.identica.type.messaging.DeliverySource; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/PendingScenarioAccountLifecycle.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/PendingScenarioAccountLifecycle.java similarity index 94% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/PendingScenarioAccountLifecycle.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/PendingScenarioAccountLifecycle.java index b087fec9c..f7799e133 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/PendingScenarioAccountLifecycle.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/PendingScenarioAccountLifecycle.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.engine.pipeline; +package me.whereareiam.identica.engine.pipeline.scenario; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -6,7 +6,6 @@ import me.whereareiam.identica.event.EventManager; import me.whereareiam.identica.event.account.AccountLifecycleEvent; import me.whereareiam.identica.event.base.IdenticEvent; -import me.whereareiam.identica.type.ScenarioResolution; import me.whereareiam.identica.event.scenario.authentication.AuthenticationResolvedEvent; import me.whereareiam.identica.event.scenario.migration.MigrationResolvedEvent; import me.whereareiam.identica.event.scenario.registration.RegistrationResolvedEvent; @@ -14,11 +13,12 @@ import me.whereareiam.identica.identity.actor.Identity; import me.whereareiam.identica.model.auth.AuthContext; import me.whereareiam.identica.model.migration.MigrationContext; -import me.whereareiam.identica.model.pipeline.state.PipelineState; -import me.whereareiam.identica.model.pipeline.state.PipelineStateReference; import me.whereareiam.identica.model.registration.RegistrationContext; import me.whereareiam.identica.pipeline.ScenarioContext; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.PipelineStateReference; import me.whereareiam.identica.pipeline.state.PipelineStateStore; +import me.whereareiam.identica.type.ScenarioResolution; import me.whereareiam.identica.type.pipeline.PipelineType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/ScenarioPipeline.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/ScenarioPipeline.java index fe63d2fff..f714a82ab 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/ScenarioPipeline.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/ScenarioPipeline.java @@ -16,9 +16,9 @@ import me.whereareiam.identica.model.auth.request.ResumeRequest; import me.whereareiam.identica.model.pipeline.PipelineResult; import me.whereareiam.identica.model.pipeline.ScenarioTransitionItem; -import me.whereareiam.identica.model.pipeline.state.PipelineState; -import me.whereareiam.identica.model.pipeline.state.PipelineStateReference; import me.whereareiam.identica.pipeline.ScenarioContext; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.PipelineStateReference; import me.whereareiam.identica.pipeline.state.PipelineStateStore; import me.whereareiam.identica.type.pipeline.PipelineType; import org.jetbrains.annotations.NotNull; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/ScenarioRegistry.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/ScenarioRegistry.java index e96757440..a68584df9 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/ScenarioRegistry.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/ScenarioRegistry.java @@ -2,15 +2,15 @@ import com.google.inject.Inject; import com.google.inject.Singleton; -import me.whereareiam.identica.engine.pipeline.scenario.authentication.AuthenticationPipeline; -import me.whereareiam.identica.engine.pipeline.scenario.migration.MigrationPipeline; -import me.whereareiam.identica.engine.pipeline.scenario.registration.RegistrationPipeline; +import me.whereareiam.identica.engine.pipeline.scenario.type.authentication.AuthenticationPipeline; +import me.whereareiam.identica.engine.pipeline.scenario.type.migration.MigrationPipeline; +import me.whereareiam.identica.engine.pipeline.scenario.type.registration.RegistrationPipeline; import me.whereareiam.identica.logging.Logger; import me.whereareiam.identica.model.auth.request.AdvanceRequest; import me.whereareiam.identica.model.auth.request.ConnectionRequest; import me.whereareiam.identica.model.auth.request.ResumeRequest; -import me.whereareiam.identica.model.pipeline.state.PipelineState; -import me.whereareiam.identica.model.pipeline.state.PipelineStateReference; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.PipelineStateReference; import me.whereareiam.identica.pipeline.state.PipelineStateStore; import me.whereareiam.identica.type.pipeline.PipelineType; import org.jetbrains.annotations.NotNull; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/identity/phase/UsernameReplicationPhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/identity/phase/UsernameReplicationPhase.java deleted file mode 100644 index 6e56a29ec..000000000 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/identity/phase/UsernameReplicationPhase.java +++ /dev/null @@ -1,129 +0,0 @@ -package me.whereareiam.identica.engine.pipeline.scenario.authentication.group.identity.phase; - -import com.google.inject.Inject; -import com.google.inject.Provider; -import com.google.inject.Singleton; -import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.engine.pipeline.scenario.authentication.group.identity.IdentityState; -import me.whereareiam.identica.engine.pipeline.scenario.base.identity.item.IdentityMetaItem; -import me.whereareiam.identica.model.config.Messages; -import me.whereareiam.identica.model.identity.Account; -import me.whereareiam.identica.model.identity.provider.AccountProviderLink; -import me.whereareiam.identica.model.identity.provider.AccountProviderProfile; -import me.whereareiam.identica.model.pipeline.PipelineResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; -import me.whereareiam.identica.model.provider.InternalProvider; -import me.whereareiam.identica.provider.ProviderManager; -import me.whereareiam.identica.type.UsernameSource; -import me.whereareiam.identica.type.pipeline.PipelineStatus; -import me.whereareiam.identica.type.provider.ProviderCapability; -import me.whereareiam.identica.pipeline.PipelinePhase; -import me.whereareiam.identica.model.pipeline.phase.PhaseResult; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.Objects; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; - -@Singleton -@RequiredArgsConstructor(onConstructor_ = @Inject) -public class UsernameReplicationPhase implements PipelinePhase { - private final ProviderManager providerManager; - private final Provider messagesProvider; - - @Override - public @NotNull String id() { - return "username-replication"; - } - - @Override - public int order() { - return 300; - } - - @Override - public @NotNull Class stateType() { - return IdentityState.class; - } - - @Override - public @NotNull CompletionStage> execute( - @NotNull PipelineState pipelineState, - @NotNull IdentityState state - ) { - PipelineResult result = state.getResult(); - if (result == null || result.getStatus() != PipelineStatus.COMPLETE) - return CompletableFuture.completedFuture(PhaseResult.pass(state)); - - Account account = state.getAccount(); - AccountProviderLink link = state.getProviderLink(); - AccountProviderProfile profile = state.getProfile(); - if (account == null || link == null || profile == null) { - state.setResult(PipelineResult.failed(identityReplicationMissingMessage())); - return CompletableFuture.completedFuture(PhaseResult.pass(state)); - } - - String previousUsername = account.getUsername(); - UsernameSource previousSource = account.getSource(); - - applyReplication(account, link, profile); - - IdentityMetaItem.Change username = IdentityMetaItem.Change.builder() - .previous(previousUsername) - .current(account.getUsername()) - .build(); - IdentityMetaItem.Change source = IdentityMetaItem.Change.builder() - .previous(previousSource.getId()) - .current(account.getSource().getId()) - .build(); - - IdentityMetaItem identity = IdentityMetaItem.builder() - .username(username) - .source(source) - .build(); - pipelineState.putItem(identity, 0L); - - return CompletableFuture.completedFuture(PhaseResult.pass(state)); - } - - private @NotNull String identityReplicationMissingMessage() { - return joinMessage(messagesProvider.get() - .getScenarios() - .getAuthentication() - .getErrors() - .getIdentity().getReplicationMissing()); - } - - private @NotNull String joinMessage(@NotNull List lines) { - return String.join("\n", lines); - } - - private void applyReplication( - @NotNull Account account, - @NotNull AccountProviderLink link, - @NotNull AccountProviderProfile profile - ) { - String providerUsername = profile.getProviderUsername(); - if (providerUsername.isBlank()) return; - if (account.getSource() == UsernameSource.MANUAL) return; - if (!link.isPrimaryLink()) return; - if (!isProviderAuthoritative(link.getProviderId())) return; - - String candidate = providerUsername.trim(); - if (candidate.equals(account.getUsername())) return; - - account.setUsername(candidate); - account.setSource(UsernameSource.PROVIDER); - } - - private boolean isProviderAuthoritative(@Nullable String providerId) { - if (providerId == null || providerId.isBlank()) return false; - return providerManager.getProviders().stream() - .map(InternalProvider::getDescriptor) - .filter(Objects::nonNull) - .anyMatch(descriptor -> descriptor.getId().equalsIgnoreCase(providerId) - && descriptor.hasCapability(ProviderCapability.AUTHORITATIVE_USERNAME)); - } -} diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/policy/PolicyState.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/policy/PolicyState.java deleted file mode 100644 index 70e7aee25..000000000 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/policy/PolicyState.java +++ /dev/null @@ -1,10 +0,0 @@ -package me.whereareiam.identica.engine.pipeline.scenario.authentication.group.policy; - -import lombok.Getter; -import lombok.Setter; -import me.whereareiam.identica.engine.pipeline.scenario.base.AbstractGroupState; - -@Getter -@Setter -public class PolicyState extends AbstractGroupState { -} diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/policy/phase/AccountReviewPhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/policy/phase/AccountReviewPhase.java deleted file mode 100644 index 17ef5c1a5..000000000 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/policy/phase/AccountReviewPhase.java +++ /dev/null @@ -1,62 +0,0 @@ -package me.whereareiam.identica.engine.pipeline.scenario.authentication.group.policy.phase; - -import com.google.inject.Inject; -import com.google.inject.Provider; -import com.google.inject.Singleton; -import me.whereareiam.identica.database.AccountPersistenceService; -import me.whereareiam.identica.database.provider.ProviderLinkPersistenceService; -import me.whereareiam.identica.database.provider.ProviderProfilePersistenceService; -import me.whereareiam.identica.engine.pipeline.scenario.authentication.group.policy.PolicyState; -import me.whereareiam.identica.engine.pipeline.scenario.base.policy.phase.base.AbstractAccountReviewPhase; -import me.whereareiam.identica.model.auth.AuthContext; -import me.whereareiam.identica.model.config.Messages; -import me.whereareiam.identica.model.pipeline.state.PipelineState; -import me.whereareiam.identica.type.pipeline.PipelineType; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -@Singleton -public class AccountReviewPhase extends AbstractAccountReviewPhase { - @Inject - public AccountReviewPhase( - AccountPersistenceService accountPersistenceService, - ProviderLinkPersistenceService providerLinkPersistenceService, - ProviderProfilePersistenceService providerProfilePersistenceService, - Provider messagesProvider - ) { - super( - accountPersistenceService, - providerLinkPersistenceService, - providerProfilePersistenceService, - messagesProvider - ); - } - - @Override - public @NotNull Class stateType() { - return PolicyState.class; - } - - @Override - protected @Nullable AuthContext resolveContext(@NotNull PipelineState pipelineState) { - PipelineType pipelineType = pipelineState.getPipelineType(); - if (pipelineType == null) - return null; - return pipelineState.getScenario(pipelineType) instanceof AuthContext authContext - ? authContext - : null; - } - - @Override - protected @NotNull String failedMessage(@NotNull Messages messages) { - return joinMessage(messages.getScenarios().getAuthentication().getAuthenticationFailed()); - } - - @Override - protected @NotNull String accountReviewMissingMessage(@NotNull Messages messages) { - return joinMessage(messages.getScenarios() - .getAuthentication() - .getErrors() - .getPolicy().getAccountReviewMissing()); - } -} diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/policy/phase/PersistUsernameChangePhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/policy/phase/PersistUsernameChangePhase.java deleted file mode 100644 index bfb4607f5..000000000 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/policy/phase/PersistUsernameChangePhase.java +++ /dev/null @@ -1,38 +0,0 @@ -package me.whereareiam.identica.engine.pipeline.scenario.authentication.group.policy.phase; - -import com.google.inject.Inject; -import com.google.inject.Singleton; -import me.whereareiam.identica.database.AccountPersistenceService; -import me.whereareiam.identica.database.UsernameHistoryPersistenceService; -import me.whereareiam.identica.engine.pipeline.scenario.authentication.group.policy.PolicyState; -import me.whereareiam.identica.engine.pipeline.scenario.base.policy.phase.base.AbstractPersistUsernameChangePhase; -import me.whereareiam.identica.model.auth.AuthContext; -import me.whereareiam.identica.model.pipeline.state.PipelineState; -import me.whereareiam.identica.type.pipeline.PipelineType; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -@Singleton -public class PersistUsernameChangePhase extends AbstractPersistUsernameChangePhase { - @Inject - public PersistUsernameChangePhase( - AccountPersistenceService accountPersistenceService, - UsernameHistoryPersistenceService usernameHistoryPersistenceService - ) { - super(accountPersistenceService, usernameHistoryPersistenceService); - } - - @Override - public @NotNull Class stateType() { - return PolicyState.class; - } - - @Override - protected @Nullable AuthContext resolveContext(@NotNull PipelineState pipelineState) { - PipelineType pipelineType = pipelineState.getPipelineType(); - if (pipelineType == null) return null; - return pipelineState.getScenario(pipelineType) instanceof AuthContext authContext - ? authContext - : null; - } -} diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/finalize/FinalizeState.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/finalize/FinalizeState.java deleted file mode 100644 index 5a87610e5..000000000 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/finalize/FinalizeState.java +++ /dev/null @@ -1,6 +0,0 @@ -package me.whereareiam.identica.engine.pipeline.scenario.base.finalize; - -import me.whereareiam.identica.engine.pipeline.scenario.base.AbstractGroupState; - -public class FinalizeState extends AbstractGroupState { -} diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/identity/item/IdentityMetaItem.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/identity/item/IdentityMetaItem.java deleted file mode 100644 index 143f39bd9..000000000 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/identity/item/IdentityMetaItem.java +++ /dev/null @@ -1,30 +0,0 @@ -package me.whereareiam.identica.engine.pipeline.scenario.base.identity.item; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import me.whereareiam.identica.pipeline.state.PipelineStateItem; - -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class IdentityMetaItem implements PipelineStateItem { - @Getter - @Setter - @NoArgsConstructor - @AllArgsConstructor - @Builder - public static class Change { - private T previous; - private T current; - } - - private String effectiveUsername; - private Change username; - private Change source; - private boolean resumed; -} diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/policy/phase/base/AbstractAccountReviewPhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/policy/phase/base/AbstractAccountReviewPhase.java deleted file mode 100644 index 049b3c2f7..000000000 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/policy/phase/base/AbstractAccountReviewPhase.java +++ /dev/null @@ -1,160 +0,0 @@ -package me.whereareiam.identica.engine.pipeline.scenario.base.policy.phase.base; - -import com.google.inject.Provider; -import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.database.AccountPersistenceService; -import me.whereareiam.identica.database.provider.ProviderLinkPersistenceService; -import me.whereareiam.identica.database.provider.ProviderProfilePersistenceService; -import me.whereareiam.identica.engine.pipeline.scenario.base.AbstractGroupState; -import me.whereareiam.identica.engine.pipeline.scenario.base.identity.item.IdentityMetaItem; -import me.whereareiam.identica.event.account.AccountPrepareEvent; -import me.whereareiam.identica.model.config.Messages; -import me.whereareiam.identica.model.identity.Account; -import me.whereareiam.identica.model.identity.AccountDecision; -import me.whereareiam.identica.model.identity.provider.AccountProviderLink; -import me.whereareiam.identica.model.identity.provider.AccountProviderProfile; -import me.whereareiam.identica.model.pipeline.PipelineResult; -import me.whereareiam.identica.model.pipeline.phase.PhaseResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; -import me.whereareiam.identica.model.provider.ProviderContext; -import me.whereareiam.identica.pipeline.PipelinePhase; -import me.whereareiam.identica.pipeline.ScenarioContext; -import me.whereareiam.identica.type.UsernameSource; -import me.whereareiam.identica.type.pipeline.PipelineStatus; -import me.whereareiam.identica.util.EventUtil; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; - -@RequiredArgsConstructor -public abstract class AbstractAccountReviewPhase implements PipelinePhase { - private final AccountPersistenceService accountPersistenceService; - private final ProviderLinkPersistenceService providerLinkPersistenceService; - private final ProviderProfilePersistenceService providerProfilePersistenceService; - private final Provider messagesProvider; - - @Override - public @NotNull String id() { - return "account-review"; - } - - @Override - public int order() { - return 100; - } - - @Override - public @NotNull CompletionStage> execute( - @NotNull PipelineState pipelineState, - @NotNull S state - ) { - PipelineResult result = state.getResult(); - if (result == null || result.getStatus() != PipelineStatus.COMPLETE) - return CompletableFuture.completedFuture(PhaseResult.pass(state)); - - IdentityMetaItem identity = pipelineState.item(IdentityMetaItem.class).orElse(null); - if (identity == null) { - state.setResult(PipelineResult.failed(accountReviewMissingMessage(messagesProvider.get()))); - return CompletableFuture.completedFuture(PhaseResult.pass(state)); - } - - C context = resolveContext(pipelineState); - ProviderContext provider = context != null ? context.getProvider() : null; - if (context == null || provider == null || context.getAccountUniqueId() == null) { - state.setResult(PipelineResult.failed(accountReviewMissingMessage(messagesProvider.get()))); - return CompletableFuture.completedFuture(PhaseResult.pass(state)); - } - if (provider.getProviderId() == null || provider.getProviderId().isBlank() - || provider.getProviderSubject() == null || provider.getProviderSubject().isBlank()) { - state.setResult(PipelineResult.failed(accountReviewMissingMessage(messagesProvider.get()))); - return CompletableFuture.completedFuture(PhaseResult.pass(state)); - } - - Account account = accountPersistenceService.findByUniqueId(context.getAccountUniqueId()).orElse(null); - if (account == null) { - state.setResult(PipelineResult.failed(accountReviewMissingMessage(messagesProvider.get()))); - return CompletableFuture.completedFuture(PhaseResult.pass(state)); - } - - Optional linkOptional = providerLinkPersistenceService - .findBySubject(provider.getProviderId(), provider.getProviderSubject()); - if (linkOptional.isEmpty()) { - state.setResult(PipelineResult.failed(accountReviewMissingMessage(messagesProvider.get()))); - return CompletableFuture.completedFuture(PhaseResult.pass(state)); - } - - AccountProviderLink link = linkOptional.get(); - AccountProviderProfile profile = providerProfilePersistenceService - .findBySubject(provider.getProviderId(), provider.getProviderSubject()) - .orElseGet(() -> AccountProviderProfile.builder() - .providerId(provider.getProviderId()) - .providerSubject(provider.getProviderSubject()) - .providerUsername(provider.getProviderUsername()) - .build()); - - IdentityMetaItem.Change usernameChange = identity.getUsername(); - IdentityMetaItem.Change sourceChange = identity.getSource(); - - if (usernameChange != null && usernameChange.getCurrent() != null) - account.setUsername(usernameChange.getCurrent()); - if (sourceChange != null && sourceChange.getCurrent() != null) - account.setSource(UsernameSource.fromId(sourceChange.getCurrent())); - - AccountPrepareEvent preparedEvent = new AccountPrepareEvent( - account.getUsername(), - provider, - account, - link, - profile, - null - ); - EventUtil.callEvent(preparedEvent); - AccountDecision decision = preparedEvent.getDecision(); - if (decision == null) decision = AccountDecision.allow(); - String effectiveUsername = preparedEvent.getEffectiveUsername(); - if (effectiveUsername == null || effectiveUsername.isBlank()) - effectiveUsername = account.getUsername(); - - if (decision.isDenied()) { - if (hasChange(usernameChange) && usernameChange.getPrevious() != null) - account.setUsername(usernameChange.getPrevious()); - if (hasChange(sourceChange) && sourceChange.getPrevious() != null) - account.setSource(UsernameSource.fromId(sourceChange.getPrevious())); - } - - identity.setEffectiveUsername(effectiveUsername); - pipelineState.putItem(identity, 0L); - - if (decision.isDenied()) { - String message = decision.getMessage(); - state.setResult(PipelineResult.denied(resolveMessage(message, messagesProvider.get()))); - } - - return CompletableFuture.completedFuture(PhaseResult.pass(state)); - } - - protected abstract @Nullable C resolveContext(@NotNull PipelineState pipelineState); - - protected abstract @NotNull String failedMessage(@NotNull Messages messages); - - protected abstract @NotNull String accountReviewMissingMessage(@NotNull Messages messages); - - private boolean hasChange(@Nullable IdentityMetaItem.Change change) { - return change != null && !Objects.equals(change.getPrevious(), change.getCurrent()); - } - - private @NotNull String resolveMessage(@Nullable String message, @NotNull Messages messages) { - if (message != null && !message.isBlank()) - return message; - return failedMessage(messages); - } - - protected final @NotNull String joinMessage(@NotNull List lines) { - return String.join("\n", lines); - } -} diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/policy/phase/base/AbstractPersistUsernameChangePhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/policy/phase/base/AbstractPersistUsernameChangePhase.java deleted file mode 100644 index 9465fdefa..000000000 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/policy/phase/base/AbstractPersistUsernameChangePhase.java +++ /dev/null @@ -1,97 +0,0 @@ -package me.whereareiam.identica.engine.pipeline.scenario.base.policy.phase.base; - -import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.database.AccountPersistenceService; -import me.whereareiam.identica.database.UsernameHistoryPersistenceService; -import me.whereareiam.identica.engine.pipeline.scenario.base.AbstractGroupState; -import me.whereareiam.identica.engine.pipeline.scenario.base.identity.item.IdentityMetaItem; -import me.whereareiam.identica.model.UsernameHistoryEntry; -import me.whereareiam.identica.model.pipeline.PipelineResult; -import me.whereareiam.identica.model.pipeline.phase.PhaseResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; -import me.whereareiam.identica.model.provider.ProviderContext; -import me.whereareiam.identica.pipeline.PipelinePhase; -import me.whereareiam.identica.pipeline.ScenarioContext; -import me.whereareiam.identica.type.UsernameSource; -import me.whereareiam.identica.type.pipeline.PipelineStatus; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.Objects; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; - -@RequiredArgsConstructor -public abstract class AbstractPersistUsernameChangePhase implements PipelinePhase { - private final AccountPersistenceService accountPersistenceService; - private final UsernameHistoryPersistenceService usernameHistoryPersistenceService; - - @Override - public @NotNull String id() { - return "persist-username-change"; - } - - @Override - public int order() { - return 200; - } - - @Override - public @NotNull CompletionStage> execute( - @NotNull PipelineState pipelineState, - @NotNull S state - ) { - PipelineResult result = state.getResult(); - if (result == null || result.getStatus() != PipelineStatus.COMPLETE) - return CompletableFuture.completedFuture(PhaseResult.pass(state)); - - IdentityMetaItem identity = pipelineState.item(IdentityMetaItem.class).orElse(null); - C context = resolveContext(pipelineState); - if (identity == null || context == null || context.getAccountUniqueId() == null) - return CompletableFuture.completedFuture(PhaseResult.pass(state)); - - IdentityMetaItem.Change usernameChange = identity.getUsername(); - IdentityMetaItem.Change sourceChange = identity.getSource(); - if (!hasChange(usernameChange)) - return CompletableFuture.completedFuture(PhaseResult.pass(state)); - - String candidate = usernameChange.getCurrent(); - if (candidate == null || candidate.isBlank()) - return CompletableFuture.completedFuture(PhaseResult.pass(state)); - - String sourceId = sourceChange != null ? sourceChange.getCurrent() : null; - if (sourceId == null || sourceId.isBlank()) - return CompletableFuture.completedFuture(PhaseResult.pass(state)); - - accountPersistenceService.updateUsername(context.getAccountUniqueId(), candidate); - accountPersistenceService.updateUsernameSource( - context.getAccountUniqueId(), - UsernameSource.fromId(sourceId) - ); - - String previous = usernameChange.getPrevious(); - if (previous == null || previous.isBlank()) - return CompletableFuture.completedFuture(PhaseResult.pass(state)); - - ProviderContext provider = context.getProvider(); - String providerId = provider != null ? provider.getProviderId() : null; - - UsernameHistoryEntry entry = UsernameHistoryEntry.builder() - .uniqueId(context.getAccountUniqueId()) - .providerId(providerId) - .oldUsername(previous) - .newUsername(candidate) - .source(sourceId) - .changedAt(System.currentTimeMillis()) - .build(); - - usernameHistoryPersistenceService.record(entry); - return CompletableFuture.completedFuture(PhaseResult.pass(state)); - } - - protected abstract @Nullable C resolveContext(@NotNull PipelineState pipelineState); - - private boolean hasChange(@Nullable IdentityMetaItem.Change change) { - return change != null && !Objects.equals(change.getPrevious(), change.getCurrent()); - } -} diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/migration/MigrationPipelineRegistry.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/migration/MigrationPipelineRegistry.java deleted file mode 100644 index 93db8259c..000000000 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/migration/MigrationPipelineRegistry.java +++ /dev/null @@ -1,75 +0,0 @@ -package me.whereareiam.identica.engine.pipeline.scenario.migration; - -import com.google.inject.Inject; -import com.google.inject.Singleton; -import me.whereareiam.identica.engine.pipeline.scenario.migration.group.identity.IdentityGroup; -import me.whereareiam.identica.engine.pipeline.scenario.migration.group.identity.phase.ApplyProviderLinkPhase; -import me.whereareiam.identica.engine.pipeline.scenario.migration.group.identity.phase.UsernameReplicationPhase; -import me.whereareiam.identica.engine.pipeline.scenario.migration.group.identity.phase.ResolveAccountPhase; -import me.whereareiam.identica.engine.pipeline.scenario.migration.group.identity.phase.ValidateProviderPhase; -import me.whereareiam.identica.engine.pipeline.scenario.migration.group.identity.phase.ValidateTargetPhase; -import me.whereareiam.identica.engine.pipeline.scenario.migration.group.session.SessionGroup; -import me.whereareiam.identica.engine.pipeline.scenario.migration.group.session.phase.BuildSessionPhase; -import me.whereareiam.identica.engine.pipeline.scenario.migration.group.session.phase.OpenSessionPhase; -import me.whereareiam.identica.engine.pipeline.scenario.base.finalize.FinalizeGroup; -import me.whereareiam.identica.engine.pipeline.scenario.base.journey.JourneyGroup; -import me.whereareiam.identica.engine.pipeline.scenario.base.journey.phase.ApplyRulesPhase; -import me.whereareiam.identica.engine.pipeline.scenario.base.journey.phase.BuildExecutionPlanPhase; -import me.whereareiam.identica.engine.pipeline.scenario.base.journey.phase.ExecutePlanPhase; -import me.whereareiam.identica.engine.pipeline.scenario.base.journey.phase.LoadContextPhase; -import me.whereareiam.identica.engine.pipeline.scenario.base.journey.phase.ResolveJourneyModePhase; -import me.whereareiam.identica.engine.pipeline.scenario.base.preparation.PreparationGroup; -import me.whereareiam.identica.engine.pipeline.scenario.registry.AbstractPipelineGroupRegistry; -import me.whereareiam.identica.pipeline.PipelineRegistry; -import me.whereareiam.identica.model.pipeline.phase.PhasePlacement; -import org.jetbrains.annotations.NotNull; - -@Singleton -public class MigrationPipelineRegistry extends AbstractPipelineGroupRegistry implements PipelineRegistry { - @Inject - public MigrationPipelineRegistry( - PreparationGroup preparationGroup, - JourneyGroup journeyGroup, - FinalizeGroup finalizeGroup, - IdentityGroup identityGroup, - SessionGroup sessionGroup, - LoadContextPhase loadContextPhase, - ResolveJourneyModePhase resolveJourneyModePhase, - BuildExecutionPlanPhase buildExecutionPlanPhase, - ApplyRulesPhase applyRulesPhase, - ExecutePlanPhase executePlanPhase, - ResolveAccountPhase resolveAccountPhase, - ValidateTargetPhase validateTargetPhase, - ValidateProviderPhase validateProviderPhase, - ApplyProviderLinkPhase applyProviderLinkPhase, - UsernameReplicationPhase usernameReplicationPhase, - BuildSessionPhase buildSessionPhase, - OpenSessionPhase openSessionPhase - ) { - register(preparationGroup); - register(journeyGroup); - register(identityGroup); - register(sessionGroup); - register(finalizeGroup); - - registerPhase(journeyGroup.id(), loadContextPhase, PhasePlacement.first()); - registerPhase(journeyGroup.id(), resolveJourneyModePhase, PhasePlacement.after(loadContextPhase.id())); - registerPhase(journeyGroup.id(), buildExecutionPlanPhase, PhasePlacement.after(resolveJourneyModePhase.id())); - registerPhase(journeyGroup.id(), applyRulesPhase, PhasePlacement.after(buildExecutionPlanPhase.id())); - registerPhase(journeyGroup.id(), executePlanPhase, PhasePlacement.last()); - - registerPhase(identityGroup.id(), resolveAccountPhase, PhasePlacement.first()); - registerPhase(identityGroup.id(), validateTargetPhase, PhasePlacement.after(resolveAccountPhase.id())); - registerPhase(identityGroup.id(), validateProviderPhase, PhasePlacement.after(validateTargetPhase.id())); - registerPhase(identityGroup.id(), applyProviderLinkPhase, PhasePlacement.after(validateProviderPhase.id())); - registerPhase(identityGroup.id(), usernameReplicationPhase, PhasePlacement.last()); - - registerPhase(sessionGroup.id(), buildSessionPhase, PhasePlacement.first()); - registerPhase(sessionGroup.id(), openSessionPhase, PhasePlacement.last()); - } - - @Override - protected @NotNull String phaseLabel() { - return "migration pipeline phase"; - } -} diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/migration/group/identity/phase/UsernameReplicationPhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/migration/group/identity/phase/UsernameReplicationPhase.java deleted file mode 100644 index 4f61a76a6..000000000 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/migration/group/identity/phase/UsernameReplicationPhase.java +++ /dev/null @@ -1,132 +0,0 @@ -package me.whereareiam.identica.engine.pipeline.scenario.migration.group.identity.phase; - -import com.google.inject.Inject; -import com.google.inject.Provider; -import com.google.inject.Singleton; -import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.engine.pipeline.scenario.migration.group.identity.IdentityState; -import me.whereareiam.identica.engine.pipeline.scenario.base.identity.item.IdentityMetaItem; -import me.whereareiam.identica.model.config.Messages; -import me.whereareiam.identica.model.identity.Account; -import me.whereareiam.identica.model.identity.provider.AccountProviderLink; -import me.whereareiam.identica.model.identity.provider.AccountProviderProfile; -import me.whereareiam.identica.model.pipeline.PipelineResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; -import me.whereareiam.identica.model.provider.InternalProvider; -import me.whereareiam.identica.provider.ProviderManager; -import me.whereareiam.identica.type.UsernameSource; -import me.whereareiam.identica.type.pipeline.PipelineStatus; -import me.whereareiam.identica.type.provider.ProviderCapability; -import me.whereareiam.identica.pipeline.PipelinePhase; -import me.whereareiam.identica.model.pipeline.phase.PhaseResult; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.List; -import java.util.Objects; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; - -@Singleton -@RequiredArgsConstructor(onConstructor_ = @Inject) -public class UsernameReplicationPhase implements PipelinePhase { - private final ProviderManager providerManager; - private final Provider messagesProvider; - - @Override - public @NotNull String id() { - return "username-replication"; - } - - @Override - public int order() { - return 500; - } - - @Override - public @NotNull Class stateType() { - return IdentityState.class; - } - - @Override - public @NotNull CompletionStage> execute( - @NotNull PipelineState pipelineState, - @NotNull IdentityState state - ) { - PipelineResult result = state.getResult(); - if (result == null || result.getStatus() != PipelineStatus.COMPLETE) - return CompletableFuture.completedFuture(PhaseResult.pass(state)); - - Account account = state.getAccount(); - AccountProviderLink link = state.getLink(); - AccountProviderProfile profile = state.getProfile(); - if (account == null || link == null || profile == null) { - state.setResult(PipelineResult.failed(identityReplicationMissingMessage())); - return CompletableFuture.completedFuture(PhaseResult.pass(state)); - } - - String previousUsername = account.getUsername(); - UsernameSource previousSource = account.getSource(); - - applyReplication(account, link, profile); - - IdentityMetaItem.Change username = IdentityMetaItem.Change.builder() - .previous(previousUsername) - .current(account.getUsername()) - .build(); - IdentityMetaItem.Change source = IdentityMetaItem.Change.builder() - .previous(previousSource.getId()) - .current(account.getSource().getId()) - .build(); - - IdentityMetaItem existing = pipelineState.item(IdentityMetaItem.class).orElse(null); - IdentityMetaItem identity = IdentityMetaItem.builder() - .effectiveUsername(existing != null ? existing.getEffectiveUsername() : null) - .resumed(existing != null && existing.isResumed()) - .username(username) - .source(source) - .build(); - pipelineState.putItem(identity, 0L); - - return CompletableFuture.completedFuture(PhaseResult.pass(state)); - } - - private @NotNull String identityReplicationMissingMessage() { - return joinMessage(messagesProvider.get() - .getScenarios() - .getMigration() - .getErrors() - .getIdentity().getReplicationMissing()); - } - - private @NotNull String joinMessage(@NotNull List lines) { - return String.join("\n", lines); - } - - private void applyReplication( - @NotNull Account account, - @NotNull AccountProviderLink link, - @NotNull AccountProviderProfile profile - ) { - String providerUsername = profile.getProviderUsername(); - if (providerUsername.isBlank()) return; - if (account.getSource() == UsernameSource.MANUAL) return; - if (!link.isPrimaryLink()) return; - if (!isProviderAuthoritative(link.getProviderId())) return; - - String candidate = providerUsername.trim(); - if (candidate.equals(account.getUsername())) return; - - account.setUsername(candidate); - account.setSource(UsernameSource.PROVIDER); - } - - private boolean isProviderAuthoritative(@Nullable String providerId) { - if (providerId == null || providerId.isBlank()) return false; - return providerManager.getProviders().stream() - .map(InternalProvider::getDescriptor) - .filter(Objects::nonNull) - .anyMatch(descriptor -> descriptor.getId().equalsIgnoreCase(providerId) - && descriptor.hasCapability(ProviderCapability.AUTHORITATIVE_USERNAME)); - } -} diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/migration/group/identity/phase/ValidateTargetPhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/migration/group/identity/phase/ValidateTargetPhase.java deleted file mode 100644 index 4b91ea8ec..000000000 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/migration/group/identity/phase/ValidateTargetPhase.java +++ /dev/null @@ -1,158 +0,0 @@ -package me.whereareiam.identica.engine.pipeline.scenario.migration.group.identity.phase; - -import com.google.inject.Inject; -import com.google.inject.Provider; -import com.google.inject.Singleton; -import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.database.provider.ProviderLinkPersistenceService; -import me.whereareiam.identica.engine.pipeline.scenario.migration.group.identity.IdentityState; -import me.whereareiam.identica.model.config.Messages; -import me.whereareiam.identica.model.identity.Account; -import me.whereareiam.identica.model.identity.provider.AccountProviderLink; -import me.whereareiam.identica.model.migration.MigrationContext; -import me.whereareiam.identica.model.pipeline.PipelineResult; -import me.whereareiam.identica.model.pipeline.migration.MigrationPendingState; -import me.whereareiam.identica.model.pipeline.phase.PhaseResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; -import me.whereareiam.identica.model.provider.InternalProvider; -import me.whereareiam.identica.pipeline.PipelinePhase; -import me.whereareiam.identica.provider.ProviderManager; -import me.whereareiam.identica.type.pipeline.PipelineStatus; -import me.whereareiam.identica.type.pipeline.PipelineType; -import me.whereareiam.identica.type.provider.ProviderCapability; -import org.jetbrains.annotations.NotNull; - -import java.util.List; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; - -@Singleton -@RequiredArgsConstructor(onConstructor_ = @Inject) -public class ValidateTargetPhase implements PipelinePhase { - private final ProviderManager providerManager; - private final ProviderLinkPersistenceService providerLinkPersistenceService; - private final Provider messagesProvider; - - @Override - public @NotNull String id() { - return "validate-target"; - } - - @Override - public int order() { - return 200; - } - - @Override - public @NotNull Class stateType() { - return IdentityState.class; - } - - @Override - public @NotNull CompletionStage> execute( - @NotNull PipelineState pipelineState, - @NotNull IdentityState state - ) { - PipelineResult result = state.getResult(); - if (result == null || result.getStatus() != PipelineStatus.COMPLETE) - return CompletableFuture.completedFuture(PhaseResult.pass(state)); - - MigrationContext context = resolveContext(pipelineState); - if (context == null) { - state.setResult(PipelineResult.failed(providerValidationMissingMessage())); - return CompletableFuture.completedFuture(PhaseResult.pass(state)); - } - - String targetProviderId = resolveTargetProviderId(context, pipelineState); - if (targetProviderId == null || targetProviderId.isBlank()) { - state.setResult(PipelineResult.failed(providerValidationMissingMessage())); - return CompletableFuture.completedFuture(PhaseResult.pass(state)); - } - - if (!supportsMigration(targetProviderId)) { - state.setResult(PipelineResult.denied(providerValidationMissingMessage())); - return CompletableFuture.completedFuture(PhaseResult.pass(state)); - } - - context.setTargetProviderId(targetProviderId); - pipelineState.setScenario(context); - state.setContext(context); - - Account account = state.getAccount(); - UUID accountId = account != null ? account.getUniqueId() : context.getAccountUniqueId(); - if (accountId == null) { - state.setResult(PipelineResult.failed(accountMissingMessage())); - return CompletableFuture.completedFuture(PhaseResult.pass(state)); - } - - AccountProviderLink existing = providerLinkPersistenceService - .findByUniqueIdAndProviderId(accountId, targetProviderId) - .orElse(null); - if (existing != null && existing.isPrimaryLink()) { - state.setResult(PipelineResult.denied(migrationFailedMessage())); - return CompletableFuture.completedFuture(PhaseResult.pass(state)); - } - - return CompletableFuture.completedFuture(PhaseResult.pass(state)); - } - - private boolean supportsMigration(@NotNull String providerId) { - for (InternalProvider provider : providerManager.findProviders(ProviderCapability.MIGRATION)) { - if (provider == null || provider.getDescriptor() == null) continue; - - String id = provider.getDescriptor().getId(); - if (id.equalsIgnoreCase(providerId)) return true; - } - return false; - } - - private String resolveTargetProviderId( - @NotNull MigrationContext context, - @NotNull PipelineState pipelineState - ) { - String targetProviderId = context.getTargetProviderId(); - if (targetProviderId != null && !targetProviderId.isBlank()) - return targetProviderId; - - MigrationPendingState pending = pipelineState.item(MigrationPendingState.class).orElse(null); - if (pending != null && !pending.getTargetProviderId().isBlank()) - return pending.getTargetProviderId(); - - if (context.getProvider() == null) return null; - return context.getProvider().getProviderId(); - } - - private @NotNull String migrationFailedMessage() { - return joinMessage(messagesProvider.get().getScenarios().getMigration().getMigrationFailed()); - } - - private @NotNull String providerValidationMissingMessage() { - return joinMessage(messagesProvider.get() - .getScenarios() - .getMigration() - .getErrors() - .getIdentity().getProvider().getValidationMissing()); - } - - private @NotNull String accountMissingMessage() { - return joinMessage(messagesProvider.get() - .getScenarios() - .getMigration() - .getErrors() - .getIdentity().getAccountMissing()); - } - - private @NotNull String joinMessage(@NotNull List lines) { - return String.join("\n", lines); - } - - private MigrationContext resolveContext(@NotNull PipelineState pipelineState) { - PipelineType pipelineType = pipelineState.getPipelineType(); - if (pipelineType == null) return null; - - return pipelineState.getScenario(pipelineType) instanceof MigrationContext migrationContext - ? migrationContext - : null; - } -} diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/identity/phase/UsernameReplicationPhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/identity/phase/UsernameReplicationPhase.java deleted file mode 100644 index 0ded1b84d..000000000 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/identity/phase/UsernameReplicationPhase.java +++ /dev/null @@ -1,132 +0,0 @@ -package me.whereareiam.identica.engine.pipeline.scenario.registration.group.identity.phase; - -import com.google.inject.Inject; -import com.google.inject.Provider; -import com.google.inject.Singleton; -import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.engine.pipeline.scenario.registration.group.identity.IdentityState; -import me.whereareiam.identica.engine.pipeline.scenario.base.identity.item.IdentityMetaItem; -import me.whereareiam.identica.model.config.Messages; -import me.whereareiam.identica.model.identity.Account; -import me.whereareiam.identica.model.identity.provider.AccountProviderLink; -import me.whereareiam.identica.model.identity.provider.AccountProviderProfile; -import me.whereareiam.identica.model.pipeline.PipelineResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; -import me.whereareiam.identica.model.provider.InternalProvider; -import me.whereareiam.identica.provider.ProviderManager; -import me.whereareiam.identica.type.UsernameSource; -import me.whereareiam.identica.type.pipeline.PipelineStatus; -import me.whereareiam.identica.type.provider.ProviderCapability; -import me.whereareiam.identica.pipeline.PipelinePhase; -import me.whereareiam.identica.model.pipeline.phase.PhaseResult; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.Objects; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; - -@Singleton -@RequiredArgsConstructor(onConstructor_ = @Inject) -public class UsernameReplicationPhase implements PipelinePhase { - private final ProviderManager providerManager; - private final Provider messagesProvider; - - @Override - public @NotNull String id() { - return "username-replication"; - } - - @Override - public int order() { - return 500; - } - - @Override - public @NotNull Class stateType() { - return IdentityState.class; - } - - @Override - public @NotNull CompletionStage> execute( - @NotNull PipelineState pipelineState, - @NotNull IdentityState state - ) { - PipelineResult result = state.getResult(); - if (result == null || result.getStatus() != PipelineStatus.COMPLETE) - return CompletableFuture.completedFuture(PhaseResult.pass(state)); - - Account account = state.getAccount(); - AccountProviderLink link = state.getLink(); - AccountProviderProfile profile = state.getProfile(); - if (account == null || link == null || profile == null) { - state.setResult(PipelineResult.failed(identityReplicationMissingMessage())); - return CompletableFuture.completedFuture(PhaseResult.pass(state)); - } - - String previousUsername = account.getUsername(); - UsernameSource previousSource = account.getSource(); - - applyReplication(account, link, profile); - - IdentityMetaItem.Change username = IdentityMetaItem.Change.builder() - .previous(previousUsername) - .current(account.getUsername()) - .build(); - IdentityMetaItem.Change source = IdentityMetaItem.Change.builder() - .previous(previousSource.getId()) - .current(account.getSource().getId()) - .build(); - - IdentityMetaItem existing = pipelineState.item(IdentityMetaItem.class).orElse(null); - IdentityMetaItem identity = IdentityMetaItem.builder() - .effectiveUsername(existing != null ? existing.getEffectiveUsername() : null) - .resumed(existing != null && existing.isResumed()) - .username(username) - .source(source) - .build(); - pipelineState.putItem(identity, 0L); - - return CompletableFuture.completedFuture(PhaseResult.pass(state)); - } - - private @NotNull String identityReplicationMissingMessage() { - return joinMessage(messagesProvider.get() - .getScenarios() - .getRegistration() - .getErrors() - .getIdentity().getReplicationMissing()); - } - - private @NotNull String joinMessage(@NotNull List lines) { - return String.join("\n", lines); - } - - private void applyReplication( - @NotNull Account account, - @NotNull AccountProviderLink link, - @NotNull AccountProviderProfile profile - ) { - String providerUsername = profile.getProviderUsername(); - if (providerUsername.isBlank()) return; - if (account.getSource() == UsernameSource.MANUAL) return; - if (!link.isPrimaryLink()) return; - if (!isProviderAuthoritative(link.getProviderId())) return; - - String candidate = providerUsername.trim(); - if (candidate.equals(account.getUsername())) return; - - account.setUsername(candidate); - account.setSource(UsernameSource.PROVIDER); - } - - private boolean isProviderAuthoritative(@Nullable String providerId) { - if (providerId == null || providerId.isBlank()) return false; - return providerManager.getProviders().stream() - .map(InternalProvider::getDescriptor) - .filter(Objects::nonNull) - .anyMatch(descriptor -> descriptor.getId().equalsIgnoreCase(providerId) - && descriptor.hasCapability(ProviderCapability.AUTHORITATIVE_USERNAME)); - } -} diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/policy/PolicyState.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/policy/PolicyState.java deleted file mode 100644 index ad126d12b..000000000 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/policy/PolicyState.java +++ /dev/null @@ -1,6 +0,0 @@ -package me.whereareiam.identica.engine.pipeline.scenario.registration.group.policy; - -import me.whereareiam.identica.engine.pipeline.scenario.base.AbstractGroupState; - -public class PolicyState extends AbstractGroupState { -} diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/policy/phase/AccountReviewPhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/policy/phase/AccountReviewPhase.java deleted file mode 100644 index 775a52366..000000000 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/policy/phase/AccountReviewPhase.java +++ /dev/null @@ -1,62 +0,0 @@ -package me.whereareiam.identica.engine.pipeline.scenario.registration.group.policy.phase; - -import com.google.inject.Inject; -import com.google.inject.Provider; -import com.google.inject.Singleton; -import me.whereareiam.identica.database.AccountPersistenceService; -import me.whereareiam.identica.database.provider.ProviderLinkPersistenceService; -import me.whereareiam.identica.database.provider.ProviderProfilePersistenceService; -import me.whereareiam.identica.engine.pipeline.scenario.registration.group.policy.PolicyState; -import me.whereareiam.identica.engine.pipeline.scenario.base.policy.phase.base.AbstractAccountReviewPhase; -import me.whereareiam.identica.model.config.Messages; -import me.whereareiam.identica.model.pipeline.state.PipelineState; -import me.whereareiam.identica.model.registration.RegistrationContext; -import me.whereareiam.identica.type.pipeline.PipelineType; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -@Singleton -public class AccountReviewPhase extends AbstractAccountReviewPhase { - @Inject - public AccountReviewPhase( - AccountPersistenceService accountPersistenceService, - ProviderLinkPersistenceService providerLinkPersistenceService, - ProviderProfilePersistenceService providerProfilePersistenceService, - Provider messagesProvider - ) { - super( - accountPersistenceService, - providerLinkPersistenceService, - providerProfilePersistenceService, - messagesProvider - ); - } - - @Override - public @NotNull Class stateType() { - return PolicyState.class; - } - - @Override - protected @Nullable RegistrationContext resolveContext(@NotNull PipelineState pipelineState) { - PipelineType pipelineType = pipelineState.getPipelineType(); - if (pipelineType == null) - return null; - return pipelineState.getScenario(pipelineType) instanceof RegistrationContext registrationContext - ? registrationContext - : null; - } - - @Override - protected @NotNull String failedMessage(@NotNull Messages messages) { - return joinMessage(messages.getScenarios().getRegistration().getRegistrationFailed()); - } - - @Override - protected @NotNull String accountReviewMissingMessage(@NotNull Messages messages) { - return joinMessage(messages.getScenarios() - .getRegistration() - .getErrors() - .getPolicy().getAccountReviewMissing()); - } -} diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/policy/phase/PersistUsernameChangePhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/policy/phase/PersistUsernameChangePhase.java deleted file mode 100644 index 41b0749a0..000000000 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/policy/phase/PersistUsernameChangePhase.java +++ /dev/null @@ -1,38 +0,0 @@ -package me.whereareiam.identica.engine.pipeline.scenario.registration.group.policy.phase; - -import com.google.inject.Inject; -import com.google.inject.Singleton; -import me.whereareiam.identica.database.AccountPersistenceService; -import me.whereareiam.identica.database.UsernameHistoryPersistenceService; -import me.whereareiam.identica.engine.pipeline.scenario.registration.group.policy.PolicyState; -import me.whereareiam.identica.engine.pipeline.scenario.base.policy.phase.base.AbstractPersistUsernameChangePhase; -import me.whereareiam.identica.model.pipeline.state.PipelineState; -import me.whereareiam.identica.model.registration.RegistrationContext; -import me.whereareiam.identica.type.pipeline.PipelineType; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -@Singleton -public class PersistUsernameChangePhase extends AbstractPersistUsernameChangePhase { - @Inject - public PersistUsernameChangePhase( - AccountPersistenceService accountPersistenceService, - UsernameHistoryPersistenceService usernameHistoryPersistenceService - ) { - super(accountPersistenceService, usernameHistoryPersistenceService); - } - - @Override - public @NotNull Class stateType() { - return PolicyState.class; - } - - @Override - protected @Nullable RegistrationContext resolveContext(@NotNull PipelineState pipelineState) { - PipelineType pipelineType = pipelineState.getPipelineType(); - if (pipelineType == null)return null; - return pipelineState.getScenario(pipelineType) instanceof RegistrationContext registrationContext - ? registrationContext - : null; - } -} diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/finalize/FinalizeGroup.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/shared/group/finalize/FinalizeGroup.java similarity index 91% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/finalize/FinalizeGroup.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/shared/group/finalize/FinalizeGroup.java index ad4d249c4..e7ab88a49 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/finalize/FinalizeGroup.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/shared/group/finalize/FinalizeGroup.java @@ -1,14 +1,15 @@ -package me.whereareiam.identica.engine.pipeline.scenario.base.finalize; +package me.whereareiam.identica.engine.pipeline.scenario.shared.group.finalize; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; import me.whereareiam.identica.model.config.Messages; -import me.whereareiam.identica.model.pipeline.PipelineResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.model.pipeline.GroupOutcome; +import me.whereareiam.identica.model.pipeline.PipelineResult; import me.whereareiam.identica.pipeline.PipelineGroup; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.shared.FinalizeState; import me.whereareiam.identica.type.pipeline.PipelineType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/journey/JourneyGroup.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/shared/group/journey/JourneyGroup.java similarity index 92% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/journey/JourneyGroup.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/shared/group/journey/JourneyGroup.java index 85352e08e..9a1777e98 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/journey/JourneyGroup.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/shared/group/journey/JourneyGroup.java @@ -1,15 +1,16 @@ -package me.whereareiam.identica.engine.pipeline.scenario.base.journey; +package me.whereareiam.identica.engine.pipeline.scenario.shared.group.journey; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; import me.whereareiam.identica.model.config.Messages; +import me.whereareiam.identica.model.pipeline.GroupOutcome; import me.whereareiam.identica.model.pipeline.PipelineResult; import me.whereareiam.identica.model.pipeline.journey.JourneyStateItem; -import me.whereareiam.identica.model.pipeline.state.PipelineState; -import me.whereareiam.identica.model.pipeline.GroupOutcome; import me.whereareiam.identica.pipeline.PipelineGroup; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.shared.JourneyState; import me.whereareiam.identica.type.pipeline.PipelineStatus; import me.whereareiam.identica.type.pipeline.PipelineType; import org.jetbrains.annotations.NotNull; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/journey/phase/ApplyRulesPhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/shared/group/journey/phase/ApplyRulesPhase.java similarity index 93% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/journey/phase/ApplyRulesPhase.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/shared/group/journey/phase/ApplyRulesPhase.java index 5d558fc6d..68abf40e9 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/journey/phase/ApplyRulesPhase.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/shared/group/journey/phase/ApplyRulesPhase.java @@ -1,36 +1,31 @@ -package me.whereareiam.identica.engine.pipeline.scenario.base.journey.phase; +package me.whereareiam.identica.engine.pipeline.scenario.shared.group.journey.phase; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.engine.pipeline.scenario.base.journey.JourneyState; -import me.whereareiam.identica.engine.pipeline.scenario.base.journey.rule.SelectProvidersRule; +import me.whereareiam.identica.engine.pipeline.scenario.shared.group.journey.rule.SelectProvidersRule; import me.whereareiam.identica.logging.Logger; import me.whereareiam.identica.model.config.Messages; import me.whereareiam.identica.model.pipeline.PipelineResult; -import me.whereareiam.identica.model.pipeline.journey.execution.JourneyExecutionStage; -import me.whereareiam.identica.pipeline.ScenarioContext; +import me.whereareiam.identica.model.pipeline.journey.JourneyRuleContext; import me.whereareiam.identica.model.pipeline.journey.JourneyStateItem; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.model.pipeline.journey.execution.JourneyExecutionBlock; import me.whereareiam.identica.model.pipeline.journey.execution.JourneyExecutionPlan; +import me.whereareiam.identica.model.pipeline.journey.execution.JourneyExecutionStage; +import me.whereareiam.identica.model.pipeline.phase.PhaseResult; +import me.whereareiam.identica.pipeline.PipelinePhase; +import me.whereareiam.identica.pipeline.ScenarioContext; import me.whereareiam.identica.pipeline.journey.rule.JourneyRule; -import me.whereareiam.identica.model.pipeline.journey.JourneyRuleContext; import me.whereareiam.identica.pipeline.journey.rule.JourneyRuleRegistry; import me.whereareiam.identica.pipeline.journey.rule.JourneyRuleScope; -import me.whereareiam.identica.pipeline.PipelinePhase; -import me.whereareiam.identica.model.pipeline.phase.PhaseResult; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.shared.JourneyState; import me.whereareiam.identica.type.pipeline.PipelineType; import me.whereareiam.identica.type.pipeline.journey.JourneyMode; import org.jetbrains.annotations.NotNull; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; +import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/journey/phase/BuildExecutionPlanPhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/shared/group/journey/phase/BuildExecutionPlanPhase.java similarity index 94% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/journey/phase/BuildExecutionPlanPhase.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/shared/group/journey/phase/BuildExecutionPlanPhase.java index 353353b40..caf57ed2c 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/journey/phase/BuildExecutionPlanPhase.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/shared/group/journey/phase/BuildExecutionPlanPhase.java @@ -1,22 +1,22 @@ -package me.whereareiam.identica.engine.pipeline.scenario.base.journey.phase; +package me.whereareiam.identica.engine.pipeline.scenario.shared.group.journey.phase; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.engine.pipeline.scenario.base.journey.JourneyState; import me.whereareiam.identica.model.config.Messages; import me.whereareiam.identica.model.pipeline.PipelineResult; -import me.whereareiam.identica.pipeline.ScenarioContext; -import me.whereareiam.identica.model.pipeline.state.PipelineState; +import me.whereareiam.identica.model.pipeline.journey.JourneyPlan; import me.whereareiam.identica.model.pipeline.journey.execution.JourneyExecutionPlan; -import me.whereareiam.identica.pipeline.PipelinePhase; import me.whereareiam.identica.model.pipeline.phase.PhaseResult; -import me.whereareiam.identica.pipeline.journey.registry.type.AuthenticationJourneyRegistry; -import me.whereareiam.identica.model.pipeline.journey.JourneyPlan; +import me.whereareiam.identica.pipeline.PipelinePhase; +import me.whereareiam.identica.pipeline.ScenarioContext; import me.whereareiam.identica.pipeline.journey.registry.JourneyRegistry; -import me.whereareiam.identica.pipeline.journey.registry.type.RegistrationJourneyRegistry; +import me.whereareiam.identica.pipeline.journey.registry.type.AuthenticationJourneyRegistry; import me.whereareiam.identica.pipeline.journey.registry.type.MigrationJourneyRegistry; +import me.whereareiam.identica.pipeline.journey.registry.type.RegistrationJourneyRegistry; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.shared.JourneyState; import me.whereareiam.identica.type.pipeline.PipelineType; import me.whereareiam.identica.type.pipeline.journey.JourneyMode; import org.jetbrains.annotations.NotNull; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/journey/phase/ExecutePlanPhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/shared/group/journey/phase/ExecutePlanPhase.java similarity index 96% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/journey/phase/ExecutePlanPhase.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/shared/group/journey/phase/ExecutePlanPhase.java index f6c26b45e..b7a926d3b 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/journey/phase/ExecutePlanPhase.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/shared/group/journey/phase/ExecutePlanPhase.java @@ -1,10 +1,9 @@ -package me.whereareiam.identica.engine.pipeline.scenario.base.journey.phase; +package me.whereareiam.identica.engine.pipeline.scenario.shared.group.journey.phase; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.engine.pipeline.scenario.base.journey.JourneyState; import me.whereareiam.identica.event.EventManager; import me.whereareiam.identica.event.pipeline.scenario.shared.ProviderSelectedEvent; import me.whereareiam.identica.event.step.StepFinishedEvent; @@ -16,8 +15,6 @@ import me.whereareiam.identica.model.config.Messages; import me.whereareiam.identica.model.pipeline.PipelineResult; import me.whereareiam.identica.model.pipeline.ScenarioTransitionItem; -import me.whereareiam.identica.model.pipeline.authentication.AuthenticationOutcomeItem; -import me.whereareiam.identica.model.pipeline.authentication.AuthenticationOutcomeItem.AuthenticationOutcome; import me.whereareiam.identica.model.pipeline.journey.JourneyOverrideItem; import me.whereareiam.identica.model.pipeline.journey.JourneyStateItem; import me.whereareiam.identica.model.pipeline.journey.execution.JourneyExecutionBlock; @@ -27,16 +24,15 @@ import me.whereareiam.identica.model.pipeline.journey.stage.step.JourneyStep; import me.whereareiam.identica.model.pipeline.journey.stage.step.StepResult; import me.whereareiam.identica.model.pipeline.phase.PhaseResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; -import me.whereareiam.identica.model.pipeline.state.PipelineStateReference; import me.whereareiam.identica.model.provider.InternalProvider; import me.whereareiam.identica.model.provider.ProviderContext; import me.whereareiam.identica.model.routing.RoutingSignal; import me.whereareiam.identica.pipeline.PipelinePhase; import me.whereareiam.identica.pipeline.ScenarioContext; -import me.whereareiam.identica.pipeline.journey.step.Step; -import me.whereareiam.identica.pipeline.journey.step.type.AuthenticationRecognitionStep; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.PipelineStateReference; import me.whereareiam.identica.pipeline.state.PipelineStateStore; +import me.whereareiam.identica.pipeline.state.scenario.shared.JourneyState; import me.whereareiam.identica.provider.ProviderManager; import me.whereareiam.identica.routing.RoutingCoordinator; import me.whereareiam.identica.type.pipeline.PipelineStatus; @@ -472,7 +468,6 @@ private FallbackOutcome(@Nullable PipelineResult result, @Nullable String failed if (status == PipelineStatus.CONTINUE) continue; if (status == PipelineStatus.COMPLETE) { - applyAuthenticationOutcome(pipelineState, pipelineType, journeyStep.getStep()); completedStage = true; break; } @@ -562,18 +557,6 @@ private FallbackOutcome(@Nullable PipelineResult result, @Nullable String failed return result; } - private void applyAuthenticationOutcome( - @NotNull PipelineState pipelineState, - @NotNull PipelineType pipelineType, - @NotNull Step step - ) { - if (pipelineType != PipelineType.AUTHENTICATION) return; - if (!(step instanceof AuthenticationRecognitionStep)) - return; - - pipelineState.putItem(new AuthenticationOutcomeItem(AuthenticationOutcome.RECOGNIZED), 0L); - } - private int resolveStartIndex( @NotNull List blocks, @Nullable JourneyStateItem pending, diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/journey/phase/LoadContextPhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/shared/group/journey/phase/LoadContextPhase.java similarity index 91% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/journey/phase/LoadContextPhase.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/shared/group/journey/phase/LoadContextPhase.java index dfe184e98..c0acebc9a 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/journey/phase/LoadContextPhase.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/shared/group/journey/phase/LoadContextPhase.java @@ -1,17 +1,17 @@ -package me.whereareiam.identica.engine.pipeline.scenario.base.journey.phase; +package me.whereareiam.identica.engine.pipeline.scenario.shared.group.journey.phase; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.engine.pipeline.scenario.base.journey.JourneyState; import me.whereareiam.identica.model.config.Messages; import me.whereareiam.identica.model.pipeline.PipelineResult; -import me.whereareiam.identica.pipeline.ScenarioContext; import me.whereareiam.identica.model.pipeline.journey.JourneyStateItem; -import me.whereareiam.identica.model.pipeline.state.PipelineState; -import me.whereareiam.identica.pipeline.PipelinePhase; import me.whereareiam.identica.model.pipeline.phase.PhaseResult; +import me.whereareiam.identica.pipeline.PipelinePhase; +import me.whereareiam.identica.pipeline.ScenarioContext; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.shared.JourneyState; import me.whereareiam.identica.type.pipeline.PipelineType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/journey/phase/ResolveJourneyModePhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/shared/group/journey/phase/ResolveJourneyModePhase.java similarity index 96% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/journey/phase/ResolveJourneyModePhase.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/shared/group/journey/phase/ResolveJourneyModePhase.java index 46f6e6e6e..f82760680 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/journey/phase/ResolveJourneyModePhase.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/shared/group/journey/phase/ResolveJourneyModePhase.java @@ -1,15 +1,13 @@ -package me.whereareiam.identica.engine.pipeline.scenario.base.journey.phase; +package me.whereareiam.identica.engine.pipeline.scenario.shared.group.journey.phase; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.engine.pipeline.scenario.base.journey.JourneyState; import me.whereareiam.identica.model.config.Engine; import me.whereareiam.identica.model.pipeline.journey.JourneyPlan; import me.whereareiam.identica.model.pipeline.journey.JourneyStateItem; import me.whereareiam.identica.model.pipeline.phase.PhaseResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.model.provider.InternalProvider; import me.whereareiam.identica.pipeline.PipelinePhase; import me.whereareiam.identica.pipeline.ScenarioContext; @@ -17,6 +15,8 @@ import me.whereareiam.identica.pipeline.journey.registry.type.AuthenticationJourneyRegistry; import me.whereareiam.identica.pipeline.journey.registry.type.MigrationJourneyRegistry; import me.whereareiam.identica.pipeline.journey.registry.type.RegistrationJourneyRegistry; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.shared.JourneyState; import me.whereareiam.identica.provider.ProviderOperations; import me.whereareiam.identica.type.pipeline.PipelineType; import me.whereareiam.identica.type.pipeline.journey.JourneyMode; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/journey/rule/DefaultJourneyRuleRegistry.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/shared/group/journey/rule/DefaultJourneyRuleRegistry.java similarity index 93% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/journey/rule/DefaultJourneyRuleRegistry.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/shared/group/journey/rule/DefaultJourneyRuleRegistry.java index 03424c59c..690d70b11 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/journey/rule/DefaultJourneyRuleRegistry.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/shared/group/journey/rule/DefaultJourneyRuleRegistry.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.engine.pipeline.scenario.base.journey.rule; +package me.whereareiam.identica.engine.pipeline.scenario.shared.group.journey.rule; import com.google.inject.Singleton; import me.whereareiam.identica.pipeline.journey.rule.JourneyRule; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/journey/rule/SelectProvidersRule.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/shared/group/journey/rule/SelectProvidersRule.java similarity index 94% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/journey/rule/SelectProvidersRule.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/shared/group/journey/rule/SelectProvidersRule.java index ed3b11b4b..068ec7c71 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/journey/rule/SelectProvidersRule.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/shared/group/journey/rule/SelectProvidersRule.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.engine.pipeline.scenario.base.journey.rule; +package me.whereareiam.identica.engine.pipeline.scenario.shared.group.journey.rule; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -12,8 +12,6 @@ import me.whereareiam.identica.model.pipeline.journey.execution.JourneyExecutionBlock; import me.whereareiam.identica.model.pipeline.journey.execution.JourneyExecutionPlan; import me.whereareiam.identica.model.pipeline.journey.execution.JourneyExecutionStage; -import me.whereareiam.identica.model.pipeline.state.PipelineState; -import me.whereareiam.identica.model.pipeline.state.PipelineStateReference; import me.whereareiam.identica.model.provider.InternalProvider; import me.whereareiam.identica.pipeline.ScenarioContext; import me.whereareiam.identica.pipeline.journey.registry.JourneyRegistry; @@ -22,13 +20,14 @@ import me.whereareiam.identica.pipeline.journey.registry.type.RegistrationJourneyRegistry; import me.whereareiam.identica.pipeline.journey.rule.JourneyRule; import me.whereareiam.identica.pipeline.journey.rule.JourneyRuleScope; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.PipelineStateReference; import me.whereareiam.identica.pipeline.state.PipelineStateStore; import me.whereareiam.identica.provider.ProviderOperations; import me.whereareiam.identica.type.pipeline.PipelineType; import me.whereareiam.identica.type.pipeline.journey.JourneyExecutionPolicy; import me.whereareiam.identica.type.pipeline.journey.JourneyMode; import me.whereareiam.identica.type.pipeline.journey.StageType; -import me.whereareiam.identica.type.provider.ProviderCapability; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -74,11 +73,6 @@ public int order() { List eligibleProviders = new ArrayList<>( providerOperations.eligibleProviders(context, pipelineType, journeyMode) ); - if (pipelineType == PipelineType.MIGRATION) { - eligibleProviders.removeIf(provider -> provider == null - || provider.getDescriptor() == null - || !provider.getDescriptor().hasCapability(ProviderCapability.MIGRATION)); - } String pendingProviderId = resolvePendingProviderId(context, pipelineType); String preferredProviderId = resolvePreferredProviderId(context, pipelineType); Set excludedProviderIds = resolveExcludedProviderIds(ctx); diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/preparation/PreparationGroup.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/shared/group/preparation/PreparationGroup.java similarity index 92% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/preparation/PreparationGroup.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/shared/group/preparation/PreparationGroup.java index cb8ae8eb9..a3cf93f8d 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/preparation/PreparationGroup.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/shared/group/preparation/PreparationGroup.java @@ -1,15 +1,16 @@ -package me.whereareiam.identica.engine.pipeline.scenario.base.preparation; +package me.whereareiam.identica.engine.pipeline.scenario.shared.group.preparation; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; import me.whereareiam.identica.model.config.Messages; -import me.whereareiam.identica.model.pipeline.PipelineResult; -import me.whereareiam.identica.pipeline.ScenarioContext; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.model.pipeline.GroupOutcome; +import me.whereareiam.identica.model.pipeline.PipelineResult; import me.whereareiam.identica.pipeline.PipelineGroup; +import me.whereareiam.identica.pipeline.ScenarioContext; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.shared.PreparationState; import me.whereareiam.identica.type.pipeline.PipelineType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/session/phase/base/AbstractBuildSessionPhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/shared/group/session/phase/base/AbstractBuildSessionPhase.java similarity index 75% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/session/phase/base/AbstractBuildSessionPhase.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/shared/group/session/phase/base/AbstractBuildSessionPhase.java index 861029041..42d16d09b 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/session/phase/base/AbstractBuildSessionPhase.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/shared/group/session/phase/base/AbstractBuildSessionPhase.java @@ -1,17 +1,16 @@ -package me.whereareiam.identica.engine.pipeline.scenario.base.session.phase.base; +package me.whereareiam.identica.engine.pipeline.scenario.shared.group.session.phase.base; import com.google.inject.Provider; import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.engine.pipeline.scenario.base.AbstractGroupState; -import me.whereareiam.identica.engine.pipeline.scenario.base.identity.item.IdentityMetaItem; import me.whereareiam.identica.model.Session; import me.whereareiam.identica.model.config.Messages; import me.whereareiam.identica.model.pipeline.PipelineResult; import me.whereareiam.identica.model.pipeline.phase.PhaseResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.model.provider.ProviderContext; import me.whereareiam.identica.pipeline.PipelinePhase; import me.whereareiam.identica.pipeline.ScenarioContext; +import me.whereareiam.identica.pipeline.state.AbstractGroupState; +import me.whereareiam.identica.pipeline.state.PipelineState; import me.whereareiam.identica.type.pipeline.PipelineStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -43,14 +42,13 @@ public int order() { return CompletableFuture.completedFuture(PhaseResult.pass(state)); C context = resolveContext(state, pipelineState); - IdentityMetaItem identity = pipelineState.item(IdentityMetaItem.class).orElse(null); - if (context == null || identity == null) { + if (context == null) { state.setResult(PipelineResult.failed(sessionBuildMissingMessage(messagesProvider.get()))); return CompletableFuture.completedFuture(PhaseResult.pass(state)); } ProviderContext provider = context.getProvider(); - String currentUsername = resolveCurrentUsername(identity); + String currentUsername = context.getUsername(); if (provider == null || isBlank(currentUsername) || context.getAccountUniqueId() == null) { state.setResult(PipelineResult.failed(sessionBuildMissingMessage(messagesProvider.get()))); return CompletableFuture.completedFuture(PhaseResult.pass(state)); @@ -61,16 +59,12 @@ public int order() { ? currentUsername : providerUsername; - String effectiveUsername = identity.getEffectiveUsername(); - if (effectiveUsername == null || effectiveUsername.isBlank()) - effectiveUsername = currentUsername; - Session session = Session.builder() .uniqueId(context.getAccountUniqueId()) .providerId(provider.getProviderId()) .providerSubject(provider.getProviderSubject()) .originalUsername(originalUsername) - .effectiveUsername(effectiveUsername) + .effectiveUsername(currentUsername) .ip(context.getIp()) .createdAt(System.currentTimeMillis()) .build(); @@ -88,11 +82,6 @@ public int order() { protected abstract @NotNull String sessionBuildMissingMessage(@NotNull Messages messages); - private @Nullable String resolveCurrentUsername(@NotNull IdentityMetaItem identity) { - IdentityMetaItem.Change username = identity.getUsername(); - return username != null ? username.getCurrent() : null; - } - private boolean isBlank(@Nullable String value) { return value == null || value.isBlank(); } diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/session/phase/base/AbstractOpenSessionPhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/shared/group/session/phase/base/AbstractOpenSessionPhase.java similarity index 89% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/session/phase/base/AbstractOpenSessionPhase.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/shared/group/session/phase/base/AbstractOpenSessionPhase.java index b5c641f8c..50774004f 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/base/session/phase/base/AbstractOpenSessionPhase.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/shared/group/session/phase/base/AbstractOpenSessionPhase.java @@ -1,9 +1,7 @@ -package me.whereareiam.identica.engine.pipeline.scenario.base.session.phase.base; +package me.whereareiam.identica.engine.pipeline.scenario.shared.group.session.phase.base; import com.google.inject.Provider; import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.engine.pipeline.scenario.base.AbstractGroupState; -import me.whereareiam.identica.engine.pipeline.scenario.base.identity.item.IdentityMetaItem; import me.whereareiam.identica.event.EventManager; import me.whereareiam.identica.event.session.SessionOpenedEvent; import me.whereareiam.identica.identity.session.SessionService; @@ -11,9 +9,11 @@ import me.whereareiam.identica.model.config.Messages; import me.whereareiam.identica.model.pipeline.PipelineResult; import me.whereareiam.identica.model.pipeline.phase.PhaseResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.pipeline.PipelinePhase; import me.whereareiam.identica.pipeline.ScenarioContext; +import me.whereareiam.identica.pipeline.state.AbstractGroupState; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.shared.item.IdentityMetaItem; import me.whereareiam.identica.type.pipeline.PipelineStatus; import me.whereareiam.identica.type.pipeline.PipelineType; import org.jetbrains.annotations.NotNull; @@ -87,8 +87,7 @@ private void publishSessionOpened( eventManager.call(new SessionOpenedEvent( connectionUniqueId, pipelineType(), - session, - false + session )); } } diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/AuthenticationPipeline.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/authentication/AuthenticationPipeline.java similarity index 80% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/AuthenticationPipeline.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/authentication/AuthenticationPipeline.java index b0d8d14a1..2d2905c6b 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/AuthenticationPipeline.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/authentication/AuthenticationPipeline.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.engine.pipeline.scenario.authentication; +package me.whereareiam.identica.engine.pipeline.scenario.type.authentication; import com.google.inject.Inject; import com.google.inject.Provider; @@ -8,8 +8,6 @@ import me.whereareiam.identica.engine.pipeline.PipelineExecutor; import me.whereareiam.identica.engine.pipeline.scenario.AbstractScenarioPipeline; import me.whereareiam.identica.event.pipeline.scenario.authentication.AuthenticationContextBuiltEvent; -import me.whereareiam.identica.engine.pipeline.scenario.base.identity.item.IdentityMetaItem; -import me.whereareiam.identica.type.ScenarioResolution; import me.whereareiam.identica.event.scenario.authentication.AuthenticationRequiredEvent; import me.whereareiam.identica.event.scenario.authentication.AuthenticationResolvedEvent; import me.whereareiam.identica.identity.actor.ConnectionIdentity; @@ -20,14 +18,15 @@ import me.whereareiam.identica.model.config.Engine; import me.whereareiam.identica.model.config.Messages; import me.whereareiam.identica.model.pipeline.journey.JourneyStateItem; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.pipeline.PipelineRegistry; import me.whereareiam.identica.pipeline.ScenarioContext; +import me.whereareiam.identica.pipeline.state.PipelineState; import me.whereareiam.identica.pipeline.state.PipelineStateStore; +import me.whereareiam.identica.pipeline.state.scenario.shared.item.IdentityMetaItem; +import me.whereareiam.identica.type.ScenarioResolution; import me.whereareiam.identica.type.pipeline.PipelineType; import me.whereareiam.identica.type.pipeline.journey.JourneyMode; import me.whereareiam.identica.util.EventUtil; -import me.whereareiam.identica.util.UniqueIdGenerator; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -60,18 +59,8 @@ public AuthenticationPipeline( @Override protected @Nullable ScenarioContext buildContext(@NotNull ConnectionRequest request) { if (request.getIdentity().getAccountUniqueId() == null) { - UUID fallbackUniqueId = request.getConnectionUniqueId(); - String username = request.getUsername(); - if (fallbackUniqueId == null && username != null) - fallbackUniqueId = UniqueIdGenerator.offlinePlayerUniqueId(username); - - if (fallbackUniqueId == null) { - Logger.severe("%s request missing Identica UUID and fallback UUID", type()); - return null; - } - - Logger.warn("%s request missing Identica UUID, applying fallback UUID %s", type(), fallbackUniqueId); - request.getIdentity().setAccountUniqueId(fallbackUniqueId); + Logger.severe("%s request missing Identica UUID", type()); + return null; } AuthContext context = AuthContext.builder() @@ -111,17 +100,13 @@ public AuthenticationPipeline( } @Override - protected boolean isPending(@NotNull PipelineState state) { + public boolean isPending(@NotNull PipelineState state) { return state.item(JourneyStateItem.class).isPresent(); } @Override protected void onStart(@NotNull PipelineState pipelineState, boolean resumed) { - IdentityMetaItem identity = pipelineState.item(IdentityMetaItem.class).orElse(null); - if (identity == null) identity = new IdentityMetaItem(); - - identity.setResumed(resumed); - pipelineState.putItem(identity, 0L); + pipelineState.putItem(new IdentityMetaItem(resumed), 0L); } @Override diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/AuthenticationPipelineRegistry.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/authentication/AuthenticationPipelineRegistry.java similarity index 52% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/AuthenticationPipelineRegistry.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/authentication/AuthenticationPipelineRegistry.java index 2e8c1af3e..1275cac0d 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/AuthenticationPipelineRegistry.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/authentication/AuthenticationPipelineRegistry.java @@ -1,20 +1,20 @@ -package me.whereareiam.identica.engine.pipeline.scenario.authentication; +package me.whereareiam.identica.engine.pipeline.scenario.type.authentication; import com.google.inject.Inject; import com.google.inject.Singleton; -import me.whereareiam.identica.engine.pipeline.scenario.authentication.group.identity.IdentityGroup; -import me.whereareiam.identica.engine.pipeline.scenario.authentication.group.identity.phase.*; -import me.whereareiam.identica.engine.pipeline.scenario.authentication.group.policy.PolicyGroup; -import me.whereareiam.identica.engine.pipeline.scenario.authentication.group.policy.phase.AccountReviewPhase; -import me.whereareiam.identica.engine.pipeline.scenario.authentication.group.policy.phase.PersistUsernameChangePhase; -import me.whereareiam.identica.engine.pipeline.scenario.authentication.group.session.SessionGroup; -import me.whereareiam.identica.engine.pipeline.scenario.authentication.group.session.phase.BuildSessionPhase; -import me.whereareiam.identica.engine.pipeline.scenario.authentication.group.session.phase.OpenSessionPhase; -import me.whereareiam.identica.engine.pipeline.scenario.base.finalize.FinalizeGroup; -import me.whereareiam.identica.engine.pipeline.scenario.base.journey.JourneyGroup; -import me.whereareiam.identica.engine.pipeline.scenario.base.journey.phase.*; -import me.whereareiam.identica.engine.pipeline.scenario.base.preparation.PreparationGroup; -import me.whereareiam.identica.engine.pipeline.scenario.registry.AbstractPipelineGroupRegistry; +import me.whereareiam.identica.engine.pipeline.AbstractPipelineGroupRegistry; +import me.whereareiam.identica.engine.pipeline.scenario.shared.group.finalize.FinalizeGroup; +import me.whereareiam.identica.engine.pipeline.scenario.shared.group.journey.JourneyGroup; +import me.whereareiam.identica.engine.pipeline.scenario.shared.group.journey.phase.*; +import me.whereareiam.identica.engine.pipeline.scenario.shared.group.preparation.PreparationGroup; +import me.whereareiam.identica.engine.pipeline.scenario.type.authentication.group.identity.IdentityGroup; +import me.whereareiam.identica.engine.pipeline.scenario.type.authentication.group.identity.phase.LoadIdentityProfilePhase; +import me.whereareiam.identica.engine.pipeline.scenario.type.authentication.group.identity.phase.RefreshProviderProfilePhase; +import me.whereareiam.identica.engine.pipeline.scenario.type.authentication.group.identity.phase.ResolveIdentityPhase; +import me.whereareiam.identica.engine.pipeline.scenario.type.authentication.group.policy.PolicyGroup; +import me.whereareiam.identica.engine.pipeline.scenario.type.authentication.group.session.SessionGroup; +import me.whereareiam.identica.engine.pipeline.scenario.type.authentication.group.session.phase.BuildSessionPhase; +import me.whereareiam.identica.engine.pipeline.scenario.type.authentication.group.session.phase.OpenSessionPhase; import me.whereareiam.identica.model.pipeline.phase.PhasePlacement; import me.whereareiam.identica.pipeline.PipelineRegistry; import org.jetbrains.annotations.NotNull; @@ -36,11 +36,7 @@ public AuthenticationPipelineRegistry( ExecutePlanPhase executePlanPhase, ResolveIdentityPhase resolveIdentityPhase, LoadIdentityProfilePhase loadIdentityProfilePhase, - EnforceProviderJoinRestrictionPhase enforceProviderJoinRestrictionPhase, RefreshProviderProfilePhase refreshProviderProfilePhase, - UsernameReplicationPhase usernameReplicationPhase, - AccountReviewPhase accountReviewPhase, - PersistUsernameChangePhase persistUsernameChangePhase, BuildSessionPhase buildSessionPhase, OpenSessionPhase openSessionPhase ) { @@ -59,12 +55,7 @@ public AuthenticationPipelineRegistry( registerPhase(identityGroup.id(), resolveIdentityPhase, PhasePlacement.first()); registerPhase(identityGroup.id(), loadIdentityProfilePhase, PhasePlacement.after(resolveIdentityPhase.id())); - registerPhase(identityGroup.id(), enforceProviderJoinRestrictionPhase, PhasePlacement.after(loadIdentityProfilePhase.id())); - registerPhase(identityGroup.id(), refreshProviderProfilePhase, PhasePlacement.after(enforceProviderJoinRestrictionPhase.id())); - registerPhase(identityGroup.id(), usernameReplicationPhase, PhasePlacement.last()); - - registerPhase(policyGroup.id(), accountReviewPhase, PhasePlacement.first()); - registerPhase(policyGroup.id(), persistUsernameChangePhase, PhasePlacement.last()); + registerPhase(identityGroup.id(), refreshProviderProfilePhase, PhasePlacement.after(loadIdentityProfilePhase.id())); registerPhase(sessionGroup.id(), buildSessionPhase, PhasePlacement.first()); registerPhase(sessionGroup.id(), openSessionPhase, PhasePlacement.last()); diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/DefaultAuthenticationStageRegistry.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/authentication/DefaultAuthenticationStageRegistry.java similarity index 90% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/DefaultAuthenticationStageRegistry.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/authentication/DefaultAuthenticationStageRegistry.java index bf868a7a5..8ffed084d 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/DefaultAuthenticationStageRegistry.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/authentication/DefaultAuthenticationStageRegistry.java @@ -1,10 +1,10 @@ -package me.whereareiam.identica.engine.pipeline.scenario.authentication; +package me.whereareiam.identica.engine.pipeline.scenario.type.authentication; import com.google.inject.Inject; import com.google.inject.Singleton; -import me.whereareiam.identica.engine.pipeline.scenario.registry.AbstractJourneyRegistry; -import me.whereareiam.identica.pipeline.journey.registry.type.AuthenticationJourneyRegistry; +import me.whereareiam.identica.engine.pipeline.scenario.AbstractJourneyRegistry; import me.whereareiam.identica.model.pipeline.journey.stage.JourneyStage; +import me.whereareiam.identica.pipeline.journey.registry.type.AuthenticationJourneyRegistry; import me.whereareiam.identica.type.pipeline.PipelineType; import me.whereareiam.identica.type.pipeline.journey.JourneyMode; import me.whereareiam.identica.type.pipeline.journey.StageType; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/identity/IdentityGroup.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/authentication/group/identity/IdentityGroup.java similarity index 87% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/identity/IdentityGroup.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/authentication/group/identity/IdentityGroup.java index e2607811c..52d367e9d 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/identity/IdentityGroup.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/authentication/group/identity/IdentityGroup.java @@ -1,14 +1,15 @@ -package me.whereareiam.identica.engine.pipeline.scenario.authentication.group.identity; +package me.whereareiam.identica.engine.pipeline.scenario.type.authentication.group.identity; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; import me.whereareiam.identica.model.config.Messages; -import me.whereareiam.identica.model.pipeline.PipelineResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.model.pipeline.GroupOutcome; +import me.whereareiam.identica.model.pipeline.PipelineResult; import me.whereareiam.identica.pipeline.PipelineGroup; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.type.authentication.IdentityState; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/identity/phase/LoadIdentityProfilePhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/authentication/group/identity/phase/LoadIdentityProfilePhase.java similarity index 92% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/identity/phase/LoadIdentityProfilePhase.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/authentication/group/identity/phase/LoadIdentityProfilePhase.java index d936bb5e8..165c23edf 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/identity/phase/LoadIdentityProfilePhase.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/authentication/group/identity/phase/LoadIdentityProfilePhase.java @@ -1,20 +1,20 @@ -package me.whereareiam.identica.engine.pipeline.scenario.authentication.group.identity.phase; +package me.whereareiam.identica.engine.pipeline.scenario.type.authentication.group.identity.phase; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; import me.whereareiam.identica.database.provider.ProviderProfilePersistenceService; -import me.whereareiam.identica.engine.pipeline.scenario.authentication.group.identity.IdentityState; import me.whereareiam.identica.model.config.Messages; import me.whereareiam.identica.model.identity.Account; import me.whereareiam.identica.model.identity.provider.AccountProviderLink; import me.whereareiam.identica.model.identity.provider.AccountProviderProfile; import me.whereareiam.identica.model.pipeline.PipelineResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; +import me.whereareiam.identica.model.pipeline.phase.PhaseResult; import me.whereareiam.identica.model.provider.ProviderContext; import me.whereareiam.identica.pipeline.PipelinePhase; -import me.whereareiam.identica.model.pipeline.phase.PhaseResult; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.type.authentication.IdentityState; import me.whereareiam.identica.type.pipeline.PipelineStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/identity/phase/RefreshProviderProfilePhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/authentication/group/identity/phase/RefreshProviderProfilePhase.java similarity index 70% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/identity/phase/RefreshProviderProfilePhase.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/authentication/group/identity/phase/RefreshProviderProfilePhase.java index b3c74e6f9..51c71ed94 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/identity/phase/RefreshProviderProfilePhase.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/authentication/group/identity/phase/RefreshProviderProfilePhase.java @@ -1,22 +1,18 @@ -package me.whereareiam.identica.engine.pipeline.scenario.authentication.group.identity.phase; +package me.whereareiam.identica.engine.pipeline.scenario.type.authentication.group.identity.phase; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; import me.whereareiam.identica.database.provider.ProviderProfilePersistenceService; -import me.whereareiam.identica.engine.pipeline.scenario.authentication.group.identity.IdentityState; -import me.whereareiam.identica.identity.actor.ConnectionIdentity; -import me.whereareiam.identica.identity.session.recognition.SessionRecognitionStore; -import me.whereareiam.identica.model.auth.AuthContext; import me.whereareiam.identica.model.config.Messages; import me.whereareiam.identica.model.identity.provider.AccountProviderProfile; import me.whereareiam.identica.model.pipeline.PipelineResult; import me.whereareiam.identica.model.pipeline.phase.PhaseResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.model.provider.ProviderContext; -import me.whereareiam.identica.model.session.SessionRecognitionSnapshot; import me.whereareiam.identica.pipeline.PipelinePhase; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.type.authentication.IdentityState; import me.whereareiam.identica.type.pipeline.PipelineStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -28,7 +24,6 @@ @RequiredArgsConstructor(onConstructor_ = @Inject) public class RefreshProviderProfilePhase implements PipelinePhase { private final ProviderProfilePersistenceService providerProfilePersistenceService; - private final SessionRecognitionStore sessionRecognitionStore; private final Provider messagesProvider; @Override @@ -65,10 +60,6 @@ public int order() { } AccountProviderProfile profile = state.getProfile(); - AuthContext authContext = pipelineState.getScenario(pipelineState.getPipelineType()) instanceof AuthContext context - ? context - : null; - ConnectionIdentity.Origin origin = authContext != null ? authContext.getIdentity().getOrigin() : null; if (profile == null) { profile = AccountProviderProfile.builder() .providerId(provider.getProviderId()) @@ -83,15 +74,6 @@ public int order() { .build(); AccountProviderProfile stored = providerProfilePersistenceService.upsert(updated); state.setProfile(stored); - sessionRecognitionStore.save(SessionRecognitionSnapshot.builder() - .providerId(provider.getProviderId()) - .providerSubject(provider.getProviderSubject()) - .providerUsername(candidate) - .lastIp(authContext != null ? authContext.getIp() : null) - .lastVirtualHost(origin != null ? origin.getHost() : null) - .lastVirtualPort(origin != null ? origin.getPort() : null) - .capturedAt(System.currentTimeMillis()) - .build()); return CompletableFuture.completedFuture(PhaseResult.pass(state)); } diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/identity/phase/ResolveIdentityPhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/authentication/group/identity/phase/ResolveIdentityPhase.java similarity index 94% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/identity/phase/ResolveIdentityPhase.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/authentication/group/identity/phase/ResolveIdentityPhase.java index b725a6292..e5d917af6 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/identity/phase/ResolveIdentityPhase.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/authentication/group/identity/phase/ResolveIdentityPhase.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.engine.pipeline.scenario.authentication.group.identity.phase; +package me.whereareiam.identica.engine.pipeline.scenario.type.authentication.group.identity.phase; import com.google.inject.Inject; import com.google.inject.Provider; @@ -6,16 +6,16 @@ import lombok.RequiredArgsConstructor; import me.whereareiam.identica.database.AccountPersistenceService; import me.whereareiam.identica.database.provider.ProviderLinkPersistenceService; -import me.whereareiam.identica.engine.pipeline.scenario.authentication.group.identity.IdentityState; import me.whereareiam.identica.model.auth.AuthContext; import me.whereareiam.identica.model.config.Messages; import me.whereareiam.identica.model.identity.Account; import me.whereareiam.identica.model.identity.provider.AccountProviderLink; import me.whereareiam.identica.model.pipeline.PipelineResult; import me.whereareiam.identica.model.pipeline.phase.PhaseResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.model.provider.ProviderContext; import me.whereareiam.identica.pipeline.PipelinePhase; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.type.authentication.IdentityState; import me.whereareiam.identica.type.pipeline.PipelineStatus; import me.whereareiam.identica.type.pipeline.PipelineType; import org.jetbrains.annotations.NotNull; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/policy/PolicyGroup.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/authentication/group/policy/PolicyGroup.java similarity index 87% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/policy/PolicyGroup.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/authentication/group/policy/PolicyGroup.java index 715a4d1d4..26229b1d7 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/policy/PolicyGroup.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/authentication/group/policy/PolicyGroup.java @@ -1,14 +1,15 @@ -package me.whereareiam.identica.engine.pipeline.scenario.authentication.group.policy; +package me.whereareiam.identica.engine.pipeline.scenario.type.authentication.group.policy; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; import me.whereareiam.identica.model.config.Messages; -import me.whereareiam.identica.model.pipeline.PipelineResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.model.pipeline.GroupOutcome; +import me.whereareiam.identica.model.pipeline.PipelineResult; import me.whereareiam.identica.pipeline.PipelineGroup; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.type.authentication.PolicyState; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/session/SessionGroup.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/authentication/group/session/SessionGroup.java similarity index 87% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/session/SessionGroup.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/authentication/group/session/SessionGroup.java index 3c8c579d2..7db68bfa7 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/session/SessionGroup.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/authentication/group/session/SessionGroup.java @@ -1,14 +1,15 @@ -package me.whereareiam.identica.engine.pipeline.scenario.authentication.group.session; +package me.whereareiam.identica.engine.pipeline.scenario.type.authentication.group.session; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; import me.whereareiam.identica.model.config.Messages; -import me.whereareiam.identica.model.pipeline.PipelineResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.model.pipeline.GroupOutcome; +import me.whereareiam.identica.model.pipeline.PipelineResult; import me.whereareiam.identica.pipeline.PipelineGroup; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.type.authentication.SessionState; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/session/phase/BuildSessionPhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/authentication/group/session/phase/BuildSessionPhase.java similarity index 78% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/session/phase/BuildSessionPhase.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/authentication/group/session/phase/BuildSessionPhase.java index 72e3e0e1e..b8b64fbb8 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/session/phase/BuildSessionPhase.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/authentication/group/session/phase/BuildSessionPhase.java @@ -1,19 +1,18 @@ -package me.whereareiam.identica.engine.pipeline.scenario.authentication.group.session.phase; +package me.whereareiam.identica.engine.pipeline.scenario.type.authentication.group.session.phase; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.engine.pipeline.scenario.authentication.group.session.SessionState; -import me.whereareiam.identica.engine.pipeline.scenario.base.identity.item.IdentityMetaItem; import me.whereareiam.identica.model.Session; import me.whereareiam.identica.model.auth.AuthContext; import me.whereareiam.identica.model.config.Messages; import me.whereareiam.identica.model.pipeline.PipelineResult; import me.whereareiam.identica.model.pipeline.phase.PhaseResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.model.provider.ProviderContext; import me.whereareiam.identica.pipeline.PipelinePhase; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.type.authentication.SessionState; import me.whereareiam.identica.type.pipeline.PipelineStatus; import me.whereareiam.identica.type.pipeline.PipelineType; import org.jetbrains.annotations.NotNull; @@ -53,9 +52,8 @@ public int order() { PipelineState source = result.getState() != null ? result.getState() : pipelineState; AuthContext authContext = resolveAuthContext(source); - IdentityMetaItem identity = pipelineState.item(IdentityMetaItem.class).orElse(null); ProviderContext provider = resolveProvider(authContext); - if (authContext == null || identity == null) { + if (authContext == null) { state.setResult(PipelineResult.failed(sessionBuildFailedMessage())); return CompletableFuture.completedFuture(PhaseResult.pass(state)); } @@ -69,18 +67,12 @@ public int order() { return CompletableFuture.completedFuture(PhaseResult.pass(state)); } - String effectiveUsername = identity.getEffectiveUsername(); - if (isBlank(effectiveUsername)) - effectiveUsername = resolveCurrentUsername(identity); - if (isBlank(effectiveUsername)) - effectiveUsername = authContext.getUsername(); - Session session = Session.builder() .uniqueId(authContext.getAccountUniqueId()) .providerId(provider.getProviderId()) .providerSubject(provider.getProviderSubject()) .originalUsername(authContext.getUsername()) - .effectiveUsername(effectiveUsername) + .effectiveUsername(authContext.getUsername()) .ip(authContext.getIp()) .createdAt(System.currentTimeMillis()) .build(); @@ -98,11 +90,6 @@ public int order() { return String.join("\n", messagesProvider.get().getScenarios().getAuthentication().getSessionBuildFailed()); } - private @Nullable String resolveCurrentUsername(@NotNull IdentityMetaItem identity) { - IdentityMetaItem.Change username = identity.getUsername(); - return username != null ? username.getCurrent() : null; - } - private @Nullable AuthContext resolveAuthContext(@NotNull PipelineState source) { PipelineType pipelineType = source.getPipelineType(); if (pipelineType == null) return null; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/session/phase/OpenSessionPhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/authentication/group/session/phase/OpenSessionPhase.java similarity index 77% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/session/phase/OpenSessionPhase.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/authentication/group/session/phase/OpenSessionPhase.java index 5d62e264a..dca656f59 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/authentication/group/session/phase/OpenSessionPhase.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/authentication/group/session/phase/OpenSessionPhase.java @@ -1,11 +1,9 @@ -package me.whereareiam.identica.engine.pipeline.scenario.authentication.group.session.phase; +package me.whereareiam.identica.engine.pipeline.scenario.type.authentication.group.session.phase; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.engine.pipeline.scenario.authentication.group.session.SessionState; -import me.whereareiam.identica.engine.pipeline.scenario.base.identity.item.IdentityMetaItem; import me.whereareiam.identica.event.EventManager; import me.whereareiam.identica.event.session.SessionOpenedEvent; import me.whereareiam.identica.identity.session.SessionService; @@ -13,11 +11,11 @@ import me.whereareiam.identica.model.auth.AuthContext; import me.whereareiam.identica.model.config.Messages; import me.whereareiam.identica.model.pipeline.PipelineResult; -import me.whereareiam.identica.model.pipeline.authentication.AuthenticationOutcomeItem; -import me.whereareiam.identica.model.pipeline.authentication.AuthenticationOutcomeItem.AuthenticationOutcome; import me.whereareiam.identica.model.pipeline.phase.PhaseResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.pipeline.PipelinePhase; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.shared.item.IdentityMetaItem; +import me.whereareiam.identica.pipeline.state.scenario.type.authentication.SessionState; import me.whereareiam.identica.type.pipeline.PipelineStatus; import me.whereareiam.identica.type.pipeline.PipelineType; import org.jetbrains.annotations.NotNull; @@ -70,12 +68,6 @@ public int order() { state.setResult(PipelineResult.failed(authenticationFailedMessage())); return CompletableFuture.completedFuture(PhaseResult.pass(state)); } - PipelineState source = result.getState() != null ? result.getState() : pipelineState; - boolean authenticationRecognized = source.item(AuthenticationOutcomeItem.class) - .map(AuthenticationOutcomeItem::getOutcome) - .map(outcome -> outcome == AuthenticationOutcome.RECOGNIZED) - .orElse(false); - return sessionService.open(session) .thenApply(openedSession -> { if (openedSession == null) { @@ -89,8 +81,7 @@ public int order() { publishSessionOpened( pipelineType, authContext.getConnectionUniqueId(), - openedSession, - authenticationRecognized + openedSession ); state.setResult(result); return PhaseResult.pass(state); @@ -100,15 +91,13 @@ public int order() { private void publishSessionOpened( @NotNull PipelineType pipelineType, UUID connectionUniqueId, - @NotNull Session session, - boolean authenticationRecognized + @NotNull Session session ) { if (connectionUniqueId == null) return; eventManager.call(new SessionOpenedEvent( connectionUniqueId, pipelineType, - session, - authenticationRecognized + session )); } diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/migration/DefaultMigrationStageRegistry.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/migration/DefaultMigrationStageRegistry.java similarity index 90% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/migration/DefaultMigrationStageRegistry.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/migration/DefaultMigrationStageRegistry.java index e07f6bdfc..1d09561ff 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/migration/DefaultMigrationStageRegistry.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/migration/DefaultMigrationStageRegistry.java @@ -1,8 +1,8 @@ -package me.whereareiam.identica.engine.pipeline.scenario.migration; +package me.whereareiam.identica.engine.pipeline.scenario.type.migration; import com.google.inject.Inject; import com.google.inject.Singleton; -import me.whereareiam.identica.engine.pipeline.scenario.registry.AbstractJourneyRegistry; +import me.whereareiam.identica.engine.pipeline.scenario.AbstractJourneyRegistry; import me.whereareiam.identica.model.pipeline.journey.stage.JourneyStage; import me.whereareiam.identica.pipeline.journey.registry.type.MigrationJourneyRegistry; import me.whereareiam.identica.type.pipeline.PipelineType; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/migration/MigrationPipeline.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/migration/MigrationPipeline.java similarity index 79% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/migration/MigrationPipeline.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/migration/MigrationPipeline.java index 2b78b6408..b2f61678f 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/migration/MigrationPipeline.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/migration/MigrationPipeline.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.engine.pipeline.scenario.migration; +package me.whereareiam.identica.engine.pipeline.scenario.type.migration; import com.google.inject.Inject; import com.google.inject.Provider; @@ -6,8 +6,6 @@ import com.google.inject.name.Named; import me.whereareiam.identica.engine.pipeline.PipelineExecutor; import me.whereareiam.identica.engine.pipeline.scenario.AbstractScenarioPipeline; -import me.whereareiam.identica.type.ScenarioResolution; -import me.whereareiam.identica.engine.pipeline.scenario.base.identity.item.IdentityMetaItem; import me.whereareiam.identica.event.scenario.migration.MigrationRequiredEvent; import me.whereareiam.identica.event.scenario.migration.MigrationResolvedEvent; import me.whereareiam.identica.identity.actor.ConnectionIdentity; @@ -18,19 +16,18 @@ import me.whereareiam.identica.model.config.Messages; import me.whereareiam.identica.model.migration.MigrationContext; import me.whereareiam.identica.model.pipeline.journey.JourneyStateItem; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.pipeline.PipelineRegistry; import me.whereareiam.identica.pipeline.ScenarioContext; +import me.whereareiam.identica.pipeline.state.PipelineState; import me.whereareiam.identica.pipeline.state.PipelineStateStore; +import me.whereareiam.identica.pipeline.state.scenario.shared.item.IdentityMetaItem; +import me.whereareiam.identica.type.ScenarioResolution; import me.whereareiam.identica.type.pipeline.PipelineType; import me.whereareiam.identica.type.pipeline.journey.JourneyMode; import me.whereareiam.identica.util.EventUtil; -import me.whereareiam.identica.util.UniqueIdGenerator; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.UUID; - @Singleton public class MigrationPipeline extends AbstractScenarioPipeline { @Inject @@ -54,18 +51,8 @@ public MigrationPipeline( @Override protected @Nullable ScenarioContext buildContext(@NotNull ConnectionRequest request) { if (request.getIdentity().getAccountUniqueId() == null) { - UUID fallbackUniqueId = request.getConnectionUniqueId(); - String username = request.getUsername(); - if (fallbackUniqueId == null && username != null) - fallbackUniqueId = UniqueIdGenerator.offlinePlayerUniqueId(username); - - if (fallbackUniqueId == null) { - Logger.severe("%s request missing Identica UUID and fallback UUID", type()); - return null; - } - - Logger.warn("%s request missing Identica UUID, applying fallback UUID %s", type(), fallbackUniqueId); - request.getIdentity().setAccountUniqueId(fallbackUniqueId); + Logger.severe("%s request missing Identica UUID", type()); + return null; } MigrationContext context = MigrationContext.builder() @@ -117,17 +104,13 @@ public MigrationPipeline( } @Override - protected boolean isPending(@NotNull PipelineState state) { + public boolean isPending(@NotNull PipelineState state) { return state.item(JourneyStateItem.class).isPresent(); } @Override protected void onStart(@NotNull PipelineState pipelineState, boolean resumed) { - IdentityMetaItem identity = pipelineState.item(IdentityMetaItem.class).orElse(null); - if (identity == null) identity = new IdentityMetaItem(); - - identity.setResumed(resumed); - pipelineState.putItem(identity, 0L); + pipelineState.putItem(new IdentityMetaItem(resumed), 0L); } @Override diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/migration/MigrationPipelineRegistry.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/migration/MigrationPipelineRegistry.java new file mode 100644 index 000000000..2fa1ea3af --- /dev/null +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/migration/MigrationPipelineRegistry.java @@ -0,0 +1,65 @@ +package me.whereareiam.identica.engine.pipeline.scenario.type.migration; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import me.whereareiam.identica.engine.pipeline.AbstractPipelineGroupRegistry; +import me.whereareiam.identica.engine.pipeline.scenario.shared.group.finalize.FinalizeGroup; +import me.whereareiam.identica.engine.pipeline.scenario.shared.group.journey.JourneyGroup; +import me.whereareiam.identica.engine.pipeline.scenario.shared.group.journey.phase.*; +import me.whereareiam.identica.engine.pipeline.scenario.shared.group.preparation.PreparationGroup; +import me.whereareiam.identica.engine.pipeline.scenario.type.migration.group.identity.IdentityGroup; +import me.whereareiam.identica.engine.pipeline.scenario.type.migration.group.identity.phase.ApplyProviderLinkPhase; +import me.whereareiam.identica.engine.pipeline.scenario.type.migration.group.identity.phase.ResolveAccountPhase; +import me.whereareiam.identica.engine.pipeline.scenario.type.migration.group.identity.phase.ValidateProviderPhase; +import me.whereareiam.identica.engine.pipeline.scenario.type.migration.group.session.SessionGroup; +import me.whereareiam.identica.engine.pipeline.scenario.type.migration.group.session.phase.BuildSessionPhase; +import me.whereareiam.identica.engine.pipeline.scenario.type.migration.group.session.phase.OpenSessionPhase; +import me.whereareiam.identica.model.pipeline.phase.PhasePlacement; +import me.whereareiam.identica.pipeline.PipelineRegistry; +import org.jetbrains.annotations.NotNull; + +@Singleton +public class MigrationPipelineRegistry extends AbstractPipelineGroupRegistry implements PipelineRegistry { + @Inject + public MigrationPipelineRegistry( + PreparationGroup preparationGroup, + JourneyGroup journeyGroup, + FinalizeGroup finalizeGroup, + IdentityGroup identityGroup, + SessionGroup sessionGroup, + LoadContextPhase loadContextPhase, + ResolveJourneyModePhase resolveJourneyModePhase, + BuildExecutionPlanPhase buildExecutionPlanPhase, + ApplyRulesPhase applyRulesPhase, + ExecutePlanPhase executePlanPhase, + ResolveAccountPhase resolveAccountPhase, + ValidateProviderPhase validateProviderPhase, + ApplyProviderLinkPhase applyProviderLinkPhase, + BuildSessionPhase buildSessionPhase, + OpenSessionPhase openSessionPhase + ) { + register(preparationGroup); + register(journeyGroup); + register(identityGroup); + register(sessionGroup); + register(finalizeGroup); + + registerPhase(journeyGroup.id(), loadContextPhase, PhasePlacement.first()); + registerPhase(journeyGroup.id(), resolveJourneyModePhase, PhasePlacement.after(loadContextPhase.id())); + registerPhase(journeyGroup.id(), buildExecutionPlanPhase, PhasePlacement.after(resolveJourneyModePhase.id())); + registerPhase(journeyGroup.id(), applyRulesPhase, PhasePlacement.after(buildExecutionPlanPhase.id())); + registerPhase(journeyGroup.id(), executePlanPhase, PhasePlacement.last()); + + registerPhase(identityGroup.id(), resolveAccountPhase, PhasePlacement.first()); + registerPhase(identityGroup.id(), validateProviderPhase, PhasePlacement.after(resolveAccountPhase.id())); + registerPhase(identityGroup.id(), applyProviderLinkPhase, PhasePlacement.after(validateProviderPhase.id())); + + registerPhase(sessionGroup.id(), buildSessionPhase, PhasePlacement.first()); + registerPhase(sessionGroup.id(), openSessionPhase, PhasePlacement.last()); + } + + @Override + protected @NotNull String phaseLabel() { + return "migration pipeline phase"; + } +} diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/migration/group/identity/IdentityGroup.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/migration/group/identity/IdentityGroup.java similarity index 87% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/migration/group/identity/IdentityGroup.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/migration/group/identity/IdentityGroup.java index 3919df465..00cad411d 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/migration/group/identity/IdentityGroup.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/migration/group/identity/IdentityGroup.java @@ -1,14 +1,15 @@ -package me.whereareiam.identica.engine.pipeline.scenario.migration.group.identity; +package me.whereareiam.identica.engine.pipeline.scenario.type.migration.group.identity; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; import me.whereareiam.identica.model.config.Messages; -import me.whereareiam.identica.model.pipeline.PipelineResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.model.pipeline.GroupOutcome; +import me.whereareiam.identica.model.pipeline.PipelineResult; import me.whereareiam.identica.pipeline.PipelineGroup; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.type.migration.IdentityState; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/migration/group/identity/phase/ApplyProviderLinkPhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/migration/group/identity/phase/ApplyProviderLinkPhase.java similarity index 93% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/migration/group/identity/phase/ApplyProviderLinkPhase.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/migration/group/identity/phase/ApplyProviderLinkPhase.java index 9e65fb583..b18af7710 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/migration/group/identity/phase/ApplyProviderLinkPhase.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/migration/group/identity/phase/ApplyProviderLinkPhase.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.engine.pipeline.scenario.migration.group.identity.phase; +package me.whereareiam.identica.engine.pipeline.scenario.type.migration.group.identity.phase; import com.google.inject.Inject; import com.google.inject.Provider; @@ -6,15 +6,15 @@ import lombok.RequiredArgsConstructor; import me.whereareiam.identica.database.provider.ProviderLinkPersistenceService; import me.whereareiam.identica.database.provider.ProviderProfilePersistenceService; -import me.whereareiam.identica.engine.pipeline.scenario.migration.group.identity.IdentityState; import me.whereareiam.identica.model.config.Messages; import me.whereareiam.identica.model.identity.Account; import me.whereareiam.identica.model.identity.provider.AccountProviderLink; import me.whereareiam.identica.model.identity.provider.AccountProviderProfile; import me.whereareiam.identica.model.pipeline.PipelineResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; -import me.whereareiam.identica.pipeline.PipelinePhase; import me.whereareiam.identica.model.pipeline.phase.PhaseResult; +import me.whereareiam.identica.pipeline.PipelinePhase; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.type.migration.IdentityState; import me.whereareiam.identica.type.pipeline.PipelineStatus; import org.jetbrains.annotations.NotNull; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/migration/group/identity/phase/ResolveAccountPhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/migration/group/identity/phase/ResolveAccountPhase.java similarity index 92% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/migration/group/identity/phase/ResolveAccountPhase.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/migration/group/identity/phase/ResolveAccountPhase.java index e38a12dee..e91dbdaa0 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/migration/group/identity/phase/ResolveAccountPhase.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/migration/group/identity/phase/ResolveAccountPhase.java @@ -1,18 +1,18 @@ -package me.whereareiam.identica.engine.pipeline.scenario.migration.group.identity.phase; +package me.whereareiam.identica.engine.pipeline.scenario.type.migration.group.identity.phase; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; import me.whereareiam.identica.database.AccountPersistenceService; -import me.whereareiam.identica.engine.pipeline.scenario.migration.group.identity.IdentityState; import me.whereareiam.identica.model.config.Messages; import me.whereareiam.identica.model.identity.Account; import me.whereareiam.identica.model.migration.MigrationContext; import me.whereareiam.identica.model.pipeline.PipelineResult; import me.whereareiam.identica.model.pipeline.phase.PhaseResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.pipeline.PipelinePhase; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.type.migration.IdentityState; import me.whereareiam.identica.type.pipeline.PipelineStatus; import me.whereareiam.identica.type.pipeline.PipelineType; import org.jetbrains.annotations.NotNull; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/migration/group/identity/phase/ValidateProviderPhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/migration/group/identity/phase/ValidateProviderPhase.java similarity index 94% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/migration/group/identity/phase/ValidateProviderPhase.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/migration/group/identity/phase/ValidateProviderPhase.java index 1af3d9ab3..7ed573504 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/migration/group/identity/phase/ValidateProviderPhase.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/migration/group/identity/phase/ValidateProviderPhase.java @@ -1,19 +1,19 @@ -package me.whereareiam.identica.engine.pipeline.scenario.migration.group.identity.phase; +package me.whereareiam.identica.engine.pipeline.scenario.type.migration.group.identity.phase; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.engine.pipeline.scenario.migration.group.identity.IdentityState; import me.whereareiam.identica.logging.Logger; import me.whereareiam.identica.model.config.Messages; import me.whereareiam.identica.model.identity.provider.AccountProviderProfile; import me.whereareiam.identica.model.migration.MigrationContext; import me.whereareiam.identica.model.pipeline.PipelineResult; import me.whereareiam.identica.model.pipeline.phase.PhaseResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.model.provider.ProviderContext; import me.whereareiam.identica.pipeline.PipelinePhase; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.type.migration.IdentityState; import me.whereareiam.identica.type.pipeline.PipelineStatus; import me.whereareiam.identica.type.pipeline.PipelineType; import org.jetbrains.annotations.NotNull; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/migration/group/session/SessionGroup.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/migration/group/session/SessionGroup.java similarity index 87% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/migration/group/session/SessionGroup.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/migration/group/session/SessionGroup.java index 438f8f3a9..5f607e85f 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/migration/group/session/SessionGroup.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/migration/group/session/SessionGroup.java @@ -1,14 +1,15 @@ -package me.whereareiam.identica.engine.pipeline.scenario.migration.group.session; +package me.whereareiam.identica.engine.pipeline.scenario.type.migration.group.session; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; import me.whereareiam.identica.model.config.Messages; -import me.whereareiam.identica.model.pipeline.PipelineResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.model.pipeline.GroupOutcome; +import me.whereareiam.identica.model.pipeline.PipelineResult; import me.whereareiam.identica.pipeline.PipelineGroup; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.type.migration.SessionState; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/migration/group/session/phase/BuildSessionPhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/migration/group/session/phase/BuildSessionPhase.java similarity index 82% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/migration/group/session/phase/BuildSessionPhase.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/migration/group/session/phase/BuildSessionPhase.java index 585e8657f..0747bb599 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/migration/group/session/phase/BuildSessionPhase.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/migration/group/session/phase/BuildSessionPhase.java @@ -1,14 +1,14 @@ -package me.whereareiam.identica.engine.pipeline.scenario.migration.group.session.phase; +package me.whereareiam.identica.engine.pipeline.scenario.type.migration.group.session.phase; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; -import me.whereareiam.identica.engine.pipeline.scenario.migration.group.session.SessionState; -import me.whereareiam.identica.engine.pipeline.scenario.base.session.phase.base.AbstractBuildSessionPhase; +import me.whereareiam.identica.engine.pipeline.scenario.shared.group.session.phase.base.AbstractBuildSessionPhase; import me.whereareiam.identica.model.Session; import me.whereareiam.identica.model.config.Messages; import me.whereareiam.identica.model.migration.MigrationContext; -import me.whereareiam.identica.model.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.type.migration.SessionState; import me.whereareiam.identica.type.pipeline.PipelineType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/migration/group/session/phase/OpenSessionPhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/migration/group/session/phase/OpenSessionPhase.java similarity index 83% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/migration/group/session/phase/OpenSessionPhase.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/migration/group/session/phase/OpenSessionPhase.java index 30352fa3d..e60e16e68 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/migration/group/session/phase/OpenSessionPhase.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/migration/group/session/phase/OpenSessionPhase.java @@ -1,15 +1,15 @@ -package me.whereareiam.identica.engine.pipeline.scenario.migration.group.session.phase; +package me.whereareiam.identica.engine.pipeline.scenario.type.migration.group.session.phase; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; -import me.whereareiam.identica.engine.pipeline.scenario.migration.group.session.SessionState; -import me.whereareiam.identica.engine.pipeline.scenario.base.session.phase.base.AbstractOpenSessionPhase; +import me.whereareiam.identica.engine.pipeline.scenario.shared.group.session.phase.base.AbstractOpenSessionPhase; import me.whereareiam.identica.event.EventManager; import me.whereareiam.identica.identity.session.SessionService; import me.whereareiam.identica.model.Session; import me.whereareiam.identica.model.config.Messages; import me.whereareiam.identica.model.migration.MigrationContext; +import me.whereareiam.identica.pipeline.state.scenario.type.migration.SessionState; import me.whereareiam.identica.type.pipeline.PipelineType; import org.jetbrains.annotations.NotNull; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/DefaultRegistrationStageRegistry.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/registration/DefaultRegistrationStageRegistry.java similarity index 92% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/DefaultRegistrationStageRegistry.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/registration/DefaultRegistrationStageRegistry.java index 726df4881..1ef0e8c74 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/DefaultRegistrationStageRegistry.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/registration/DefaultRegistrationStageRegistry.java @@ -1,8 +1,8 @@ -package me.whereareiam.identica.engine.pipeline.scenario.registration; +package me.whereareiam.identica.engine.pipeline.scenario.type.registration; import com.google.inject.Inject; import com.google.inject.Singleton; -import me.whereareiam.identica.engine.pipeline.scenario.registry.AbstractJourneyRegistry; +import me.whereareiam.identica.engine.pipeline.scenario.AbstractJourneyRegistry; import me.whereareiam.identica.engine.step.EnrollmentStep; import me.whereareiam.identica.model.pipeline.journey.stage.JourneyStage; import me.whereareiam.identica.model.pipeline.journey.stage.step.JourneyStep; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/RegistrationPipeline.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/registration/RegistrationPipeline.java similarity index 81% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/RegistrationPipeline.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/registration/RegistrationPipeline.java index d8514fef6..8f65b68e1 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/RegistrationPipeline.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/registration/RegistrationPipeline.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.engine.pipeline.scenario.registration; +package me.whereareiam.identica.engine.pipeline.scenario.type.registration; import com.google.inject.Inject; import com.google.inject.Provider; @@ -8,8 +8,6 @@ import me.whereareiam.identica.engine.pipeline.PipelineExecutor; import me.whereareiam.identica.engine.pipeline.scenario.AbstractScenarioPipeline; import me.whereareiam.identica.event.pipeline.scenario.registration.RegistrationContextBuiltEvent; -import me.whereareiam.identica.engine.pipeline.scenario.base.identity.item.IdentityMetaItem; -import me.whereareiam.identica.type.ScenarioResolution; import me.whereareiam.identica.event.scenario.registration.RegistrationRequiredEvent; import me.whereareiam.identica.event.scenario.registration.RegistrationResolvedEvent; import me.whereareiam.identica.identity.actor.ConnectionIdentity; @@ -19,15 +17,16 @@ import me.whereareiam.identica.model.config.Engine; import me.whereareiam.identica.model.config.Messages; import me.whereareiam.identica.model.pipeline.journey.JourneyStateItem; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.model.registration.RegistrationContext; import me.whereareiam.identica.pipeline.PipelineRegistry; import me.whereareiam.identica.pipeline.ScenarioContext; +import me.whereareiam.identica.pipeline.state.PipelineState; import me.whereareiam.identica.pipeline.state.PipelineStateStore; +import me.whereareiam.identica.pipeline.state.scenario.shared.item.IdentityMetaItem; +import me.whereareiam.identica.type.ScenarioResolution; import me.whereareiam.identica.type.pipeline.PipelineType; import me.whereareiam.identica.type.pipeline.journey.JourneyMode; import me.whereareiam.identica.util.EventUtil; -import me.whereareiam.identica.util.UniqueIdGenerator; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -60,18 +59,8 @@ public RegistrationPipeline( @Override protected @Nullable ScenarioContext buildContext(@NotNull ConnectionRequest request) { if (request.getIdentity().getAccountUniqueId() == null) { - UUID fallbackUniqueId = request.getConnectionUniqueId(); - String username = request.getUsername(); - if (fallbackUniqueId == null && username != null) - fallbackUniqueId = UniqueIdGenerator.offlinePlayerUniqueId(username); - - if (fallbackUniqueId == null) { - Logger.severe("%s request missing Identica UUID and fallback UUID", type()); - return null; - } - - Logger.warn("%s request missing Identica UUID, applying fallback UUID %s", type(), fallbackUniqueId); - request.getIdentity().setAccountUniqueId(fallbackUniqueId); + Logger.severe("%s request missing Identica UUID", type()); + return null; } RegistrationContext context = RegistrationContext.builder() @@ -113,17 +102,13 @@ public RegistrationPipeline( } @Override - protected boolean isPending(@NotNull PipelineState state) { + public boolean isPending(@NotNull PipelineState state) { return state.item(JourneyStateItem.class).isPresent(); } @Override protected void onStart(@NotNull PipelineState pipelineState, boolean resumed) { - IdentityMetaItem identity = pipelineState.item(IdentityMetaItem.class).orElse(null); - if (identity == null) identity = new IdentityMetaItem(); - - identity.setResumed(resumed); - pipelineState.putItem(identity, 0L); + pipelineState.putItem(new IdentityMetaItem(resumed), 0L); } @Override diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/RegistrationPipelineRegistry.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/registration/RegistrationPipelineRegistry.java similarity index 53% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/RegistrationPipelineRegistry.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/registration/RegistrationPipelineRegistry.java index eef34757a..214cd1fc9 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/RegistrationPipelineRegistry.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/registration/RegistrationPipelineRegistry.java @@ -1,20 +1,21 @@ -package me.whereareiam.identica.engine.pipeline.scenario.registration; +package me.whereareiam.identica.engine.pipeline.scenario.type.registration; import com.google.inject.Inject; import com.google.inject.Singleton; -import me.whereareiam.identica.engine.pipeline.scenario.registration.group.identity.IdentityGroup; -import me.whereareiam.identica.engine.pipeline.scenario.registration.group.identity.phase.*; -import me.whereareiam.identica.engine.pipeline.scenario.registration.group.policy.PolicyGroup; -import me.whereareiam.identica.engine.pipeline.scenario.registration.group.policy.phase.AccountReviewPhase; -import me.whereareiam.identica.engine.pipeline.scenario.registration.group.policy.phase.PersistUsernameChangePhase; -import me.whereareiam.identica.engine.pipeline.scenario.registration.group.session.SessionGroup; -import me.whereareiam.identica.engine.pipeline.scenario.registration.group.session.phase.BuildSessionPhase; -import me.whereareiam.identica.engine.pipeline.scenario.registration.group.session.phase.OpenSessionPhase; -import me.whereareiam.identica.engine.pipeline.scenario.base.finalize.FinalizeGroup; -import me.whereareiam.identica.engine.pipeline.scenario.base.journey.JourneyGroup; -import me.whereareiam.identica.engine.pipeline.scenario.base.journey.phase.*; -import me.whereareiam.identica.engine.pipeline.scenario.base.preparation.PreparationGroup; -import me.whereareiam.identica.engine.pipeline.scenario.registry.AbstractPipelineGroupRegistry; +import me.whereareiam.identica.engine.pipeline.AbstractPipelineGroupRegistry; +import me.whereareiam.identica.engine.pipeline.scenario.shared.group.finalize.FinalizeGroup; +import me.whereareiam.identica.engine.pipeline.scenario.shared.group.journey.JourneyGroup; +import me.whereareiam.identica.engine.pipeline.scenario.shared.group.journey.phase.*; +import me.whereareiam.identica.engine.pipeline.scenario.shared.group.preparation.PreparationGroup; +import me.whereareiam.identica.engine.pipeline.scenario.type.registration.group.identity.IdentityGroup; +import me.whereareiam.identica.engine.pipeline.scenario.type.registration.group.identity.phase.CreateAccountPhase; +import me.whereareiam.identica.engine.pipeline.scenario.type.registration.group.identity.phase.EnsureNewAccountPhase; +import me.whereareiam.identica.engine.pipeline.scenario.type.registration.group.identity.phase.LinkProviderPhase; +import me.whereareiam.identica.engine.pipeline.scenario.type.registration.group.identity.phase.ValidateProviderPhase; +import me.whereareiam.identica.engine.pipeline.scenario.type.registration.group.policy.PolicyGroup; +import me.whereareiam.identica.engine.pipeline.scenario.type.registration.group.session.SessionGroup; +import me.whereareiam.identica.engine.pipeline.scenario.type.registration.group.session.phase.BuildSessionPhase; +import me.whereareiam.identica.engine.pipeline.scenario.type.registration.group.session.phase.OpenSessionPhase; import me.whereareiam.identica.model.pipeline.phase.PhasePlacement; import me.whereareiam.identica.pipeline.PipelineRegistry; import org.jetbrains.annotations.NotNull; @@ -35,13 +36,9 @@ public RegistrationPipelineRegistry( ApplyRulesPhase applyRulesPhase, ExecutePlanPhase executePlanPhase, ValidateProviderPhase validateProviderPhase, - EnforceProviderJoinRestrictionPhase enforceProviderJoinRestrictionPhase, EnsureNewAccountPhase ensureNewAccountPhase, CreateAccountPhase createAccountPhase, LinkProviderPhase linkProviderPhase, - UsernameReplicationPhase usernameReplicationPhase, - AccountReviewPhase accountReviewPhase, - PersistUsernameChangePhase persistUsernameChangePhase, BuildSessionPhase buildSessionPhase, OpenSessionPhase openSessionPhase ) { @@ -59,14 +56,9 @@ public RegistrationPipelineRegistry( registerPhase(journeyGroup.id(), executePlanPhase, PhasePlacement.last()); registerPhase(identityGroup.id(), validateProviderPhase, PhasePlacement.first()); - registerPhase(identityGroup.id(), enforceProviderJoinRestrictionPhase, PhasePlacement.after(validateProviderPhase.id())); - registerPhase(identityGroup.id(), ensureNewAccountPhase, PhasePlacement.after(enforceProviderJoinRestrictionPhase.id())); + registerPhase(identityGroup.id(), ensureNewAccountPhase, PhasePlacement.after(validateProviderPhase.id())); registerPhase(identityGroup.id(), createAccountPhase, PhasePlacement.after(ensureNewAccountPhase.id())); registerPhase(identityGroup.id(), linkProviderPhase, PhasePlacement.after(createAccountPhase.id())); - registerPhase(identityGroup.id(), usernameReplicationPhase, PhasePlacement.last()); - - registerPhase(policyGroup.id(), accountReviewPhase, PhasePlacement.first()); - registerPhase(policyGroup.id(), persistUsernameChangePhase, PhasePlacement.last()); registerPhase(sessionGroup.id(), buildSessionPhase, PhasePlacement.first()); registerPhase(sessionGroup.id(), openSessionPhase, PhasePlacement.last()); diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/identity/IdentityGroup.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/registration/group/identity/IdentityGroup.java similarity index 87% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/identity/IdentityGroup.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/registration/group/identity/IdentityGroup.java index 8239b0ef8..81e1d7a00 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/identity/IdentityGroup.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/registration/group/identity/IdentityGroup.java @@ -1,14 +1,15 @@ -package me.whereareiam.identica.engine.pipeline.scenario.registration.group.identity; +package me.whereareiam.identica.engine.pipeline.scenario.type.registration.group.identity; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; import me.whereareiam.identica.model.config.Messages; -import me.whereareiam.identica.model.pipeline.PipelineResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.model.pipeline.GroupOutcome; +import me.whereareiam.identica.model.pipeline.PipelineResult; import me.whereareiam.identica.pipeline.PipelineGroup; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.type.registration.IdentityState; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/identity/phase/CreateAccountPhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/registration/group/identity/phase/CreateAccountPhase.java similarity index 92% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/identity/phase/CreateAccountPhase.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/registration/group/identity/phase/CreateAccountPhase.java index 5d37a32ed..a2b64c209 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/identity/phase/CreateAccountPhase.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/registration/group/identity/phase/CreateAccountPhase.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.engine.pipeline.scenario.registration.group.identity.phase; +package me.whereareiam.identica.engine.pipeline.scenario.type.registration.group.identity.phase; import com.google.inject.Inject; import com.google.inject.Provider; @@ -6,16 +6,15 @@ import lombok.RequiredArgsConstructor; import me.whereareiam.identica.database.AccountPersistenceService; import me.whereareiam.identica.database.AccountReservationPersistenceService; -import me.whereareiam.identica.engine.pipeline.scenario.registration.group.identity.IdentityState; import me.whereareiam.identica.model.config.Messages; import me.whereareiam.identica.model.identity.Account; import me.whereareiam.identica.model.identity.provider.AccountProviderProfile; import me.whereareiam.identica.model.pipeline.PipelineResult; import me.whereareiam.identica.model.pipeline.phase.PhaseResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.model.registration.RegistrationContext; import me.whereareiam.identica.pipeline.PipelinePhase; -import me.whereareiam.identica.type.UsernameSource; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.type.registration.IdentityState; import me.whereareiam.identica.type.pipeline.PipelineStatus; import me.whereareiam.identica.util.UniqueIdGenerator; import org.jetbrains.annotations.NotNull; @@ -83,7 +82,6 @@ public int order() { Account account = Account.builder() .uniqueId(uniqueId) .username(profile.getProviderUsername()) - .source(UsernameSource.PROVIDER) .createdAt(now) .lastSeenAt(now) .build(); diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/identity/phase/EnsureNewAccountPhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/registration/group/identity/phase/EnsureNewAccountPhase.java similarity index 94% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/identity/phase/EnsureNewAccountPhase.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/registration/group/identity/phase/EnsureNewAccountPhase.java index 646cd613c..d81cf2365 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/identity/phase/EnsureNewAccountPhase.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/registration/group/identity/phase/EnsureNewAccountPhase.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.engine.pipeline.scenario.registration.group.identity.phase; +package me.whereareiam.identica.engine.pipeline.scenario.type.registration.group.identity.phase; import com.google.inject.Inject; import com.google.inject.Provider; @@ -6,17 +6,17 @@ import lombok.RequiredArgsConstructor; import me.whereareiam.identica.database.AccountPersistenceService; import me.whereareiam.identica.database.provider.ProviderLinkPersistenceService; -import me.whereareiam.identica.engine.pipeline.scenario.registration.group.identity.IdentityState; import me.whereareiam.identica.model.config.Messages; import me.whereareiam.identica.model.identity.Account; import me.whereareiam.identica.model.identity.provider.AccountProviderLink; import me.whereareiam.identica.model.pipeline.PipelineResult; import me.whereareiam.identica.model.pipeline.ScenarioTransitionItem; import me.whereareiam.identica.model.pipeline.phase.PhaseResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.model.provider.ProviderContext; import me.whereareiam.identica.model.registration.RegistrationContext; import me.whereareiam.identica.pipeline.PipelinePhase; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.type.registration.IdentityState; import me.whereareiam.identica.type.pipeline.PipelineStatus; import me.whereareiam.identica.type.pipeline.PipelineType; import org.jetbrains.annotations.NotNull; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/identity/phase/LinkProviderPhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/registration/group/identity/phase/LinkProviderPhase.java similarity index 77% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/identity/phase/LinkProviderPhase.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/registration/group/identity/phase/LinkProviderPhase.java index 7b54daedc..85651e7e0 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/identity/phase/LinkProviderPhase.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/registration/group/identity/phase/LinkProviderPhase.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.engine.pipeline.scenario.registration.group.identity.phase; +package me.whereareiam.identica.engine.pipeline.scenario.type.registration.group.identity.phase; import com.google.inject.Inject; import com.google.inject.Provider; @@ -6,18 +6,15 @@ import lombok.RequiredArgsConstructor; import me.whereareiam.identica.database.provider.ProviderLinkPersistenceService; import me.whereareiam.identica.database.provider.ProviderProfilePersistenceService; -import me.whereareiam.identica.engine.pipeline.scenario.registration.group.identity.IdentityState; -import me.whereareiam.identica.identity.actor.ConnectionIdentity; -import me.whereareiam.identica.identity.session.recognition.SessionRecognitionStore; import me.whereareiam.identica.model.config.Messages; import me.whereareiam.identica.model.identity.Account; import me.whereareiam.identica.model.identity.provider.AccountProviderLink; import me.whereareiam.identica.model.identity.provider.AccountProviderProfile; import me.whereareiam.identica.model.pipeline.PipelineResult; import me.whereareiam.identica.model.pipeline.phase.PhaseResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; -import me.whereareiam.identica.model.session.SessionRecognitionSnapshot; import me.whereareiam.identica.pipeline.PipelinePhase; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.type.registration.IdentityState; import me.whereareiam.identica.type.pipeline.PipelineStatus; import org.jetbrains.annotations.NotNull; @@ -29,7 +26,6 @@ public class LinkProviderPhase implements PipelinePhase { private final ProviderLinkPersistenceService providerLinkPersistenceService; private final ProviderProfilePersistenceService providerProfilePersistenceService; - private final SessionRecognitionStore sessionRecognitionStore; private final Provider messagesProvider; @Override @@ -84,16 +80,6 @@ public int order() { AccountProviderLink stored = providerLinkPersistenceService.upsert(link); AccountProviderProfile storedProfile = providerProfilePersistenceService.upsert(profile); - ConnectionIdentity.Origin origin = state.getContext().getIdentity().getOrigin(); - sessionRecognitionStore.save(SessionRecognitionSnapshot.builder() - .providerId(profile.getProviderId()) - .providerSubject(profile.getProviderSubject()) - .providerUsername(profile.getProviderUsername()) - .lastIp(state.getContext().getIp()) - .lastVirtualHost(origin != null ? origin.getHost() : null) - .lastVirtualPort(origin != null ? origin.getPort() : null) - .capturedAt(now) - .build()); state.setLink(stored); state.setProfile(storedProfile); diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/identity/phase/ValidateProviderPhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/registration/group/identity/phase/ValidateProviderPhase.java similarity index 92% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/identity/phase/ValidateProviderPhase.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/registration/group/identity/phase/ValidateProviderPhase.java index 16dde3bc1..ff2cc25f9 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/identity/phase/ValidateProviderPhase.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/registration/group/identity/phase/ValidateProviderPhase.java @@ -1,18 +1,18 @@ -package me.whereareiam.identica.engine.pipeline.scenario.registration.group.identity.phase; +package me.whereareiam.identica.engine.pipeline.scenario.type.registration.group.identity.phase; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.engine.pipeline.scenario.registration.group.identity.IdentityState; import me.whereareiam.identica.model.config.Messages; import me.whereareiam.identica.model.identity.provider.AccountProviderProfile; import me.whereareiam.identica.model.pipeline.PipelineResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; +import me.whereareiam.identica.model.pipeline.phase.PhaseResult; import me.whereareiam.identica.model.provider.ProviderContext; import me.whereareiam.identica.model.registration.RegistrationContext; import me.whereareiam.identica.pipeline.PipelinePhase; -import me.whereareiam.identica.model.pipeline.phase.PhaseResult; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.type.registration.IdentityState; import me.whereareiam.identica.type.pipeline.PipelineStatus; import me.whereareiam.identica.type.pipeline.PipelineType; import org.jetbrains.annotations.NotNull; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/policy/PolicyGroup.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/registration/group/policy/PolicyGroup.java similarity index 87% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/policy/PolicyGroup.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/registration/group/policy/PolicyGroup.java index 34e991fcb..fbc085c3e 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/policy/PolicyGroup.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/registration/group/policy/PolicyGroup.java @@ -1,14 +1,15 @@ -package me.whereareiam.identica.engine.pipeline.scenario.registration.group.policy; +package me.whereareiam.identica.engine.pipeline.scenario.type.registration.group.policy; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; import me.whereareiam.identica.model.config.Messages; -import me.whereareiam.identica.model.pipeline.PipelineResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.model.pipeline.GroupOutcome; +import me.whereareiam.identica.model.pipeline.PipelineResult; import me.whereareiam.identica.pipeline.PipelineGroup; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.type.registration.PolicyState; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/session/SessionGroup.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/registration/group/session/SessionGroup.java similarity index 87% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/session/SessionGroup.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/registration/group/session/SessionGroup.java index 7f71dd41e..9fd61f4b6 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/session/SessionGroup.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/registration/group/session/SessionGroup.java @@ -1,14 +1,15 @@ -package me.whereareiam.identica.engine.pipeline.scenario.registration.group.session; +package me.whereareiam.identica.engine.pipeline.scenario.type.registration.group.session; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; import me.whereareiam.identica.model.config.Messages; -import me.whereareiam.identica.model.pipeline.PipelineResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.model.pipeline.GroupOutcome; +import me.whereareiam.identica.model.pipeline.PipelineResult; import me.whereareiam.identica.pipeline.PipelineGroup; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.type.registration.SessionState; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/session/phase/BuildSessionPhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/registration/group/session/phase/BuildSessionPhase.java similarity index 82% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/session/phase/BuildSessionPhase.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/registration/group/session/phase/BuildSessionPhase.java index a61689f37..74e91a090 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/session/phase/BuildSessionPhase.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/registration/group/session/phase/BuildSessionPhase.java @@ -1,14 +1,14 @@ -package me.whereareiam.identica.engine.pipeline.scenario.registration.group.session.phase; +package me.whereareiam.identica.engine.pipeline.scenario.type.registration.group.session.phase; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; -import me.whereareiam.identica.engine.pipeline.scenario.registration.group.session.SessionState; -import me.whereareiam.identica.engine.pipeline.scenario.base.session.phase.base.AbstractBuildSessionPhase; +import me.whereareiam.identica.engine.pipeline.scenario.shared.group.session.phase.base.AbstractBuildSessionPhase; import me.whereareiam.identica.model.Session; import me.whereareiam.identica.model.config.Messages; -import me.whereareiam.identica.model.pipeline.state.PipelineState; import me.whereareiam.identica.model.registration.RegistrationContext; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.type.registration.SessionState; import me.whereareiam.identica.type.pipeline.PipelineType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/session/phase/OpenSessionPhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/registration/group/session/phase/OpenSessionPhase.java similarity index 83% rename from identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/session/phase/OpenSessionPhase.java rename to identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/registration/group/session/phase/OpenSessionPhase.java index 060271c54..8f95a71d1 100644 --- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/registration/group/session/phase/OpenSessionPhase.java +++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/scenario/type/registration/group/session/phase/OpenSessionPhase.java @@ -1,15 +1,15 @@ -package me.whereareiam.identica.engine.pipeline.scenario.registration.group.session.phase; +package me.whereareiam.identica.engine.pipeline.scenario.type.registration.group.session.phase; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; -import me.whereareiam.identica.engine.pipeline.scenario.registration.group.session.SessionState; -import me.whereareiam.identica.engine.pipeline.scenario.base.session.phase.base.AbstractOpenSessionPhase; +import me.whereareiam.identica.engine.pipeline.scenario.shared.group.session.phase.base.AbstractOpenSessionPhase; import me.whereareiam.identica.event.EventManager; import me.whereareiam.identica.identity.session.SessionService; import me.whereareiam.identica.model.Session; import me.whereareiam.identica.model.config.Messages; import me.whereareiam.identica.model.registration.RegistrationContext; +import me.whereareiam.identica.pipeline.state.scenario.type.registration.SessionState; import me.whereareiam.identica.type.pipeline.PipelineType; import org.jetbrains.annotations.NotNull; diff --git a/identica-engine/src/test/java/me/whereareiam/identica/engine/completion/CompletionPendingLifecycleTest.java b/identica-engine/src/test/java/me/whereareiam/identica/engine/completion/CompletionPendingLifecycleTest.java index 5b0a91289..916166dd8 100644 --- a/identica-engine/src/test/java/me/whereareiam/identica/engine/completion/CompletionPendingLifecycleTest.java +++ b/identica-engine/src/test/java/me/whereareiam/identica/engine/completion/CompletionPendingLifecycleTest.java @@ -1,7 +1,7 @@ package me.whereareiam.identica.engine.completion; -import me.whereareiam.identica.engine.pipeline.completion.CompletionPendingLifecycle; -import me.whereareiam.identica.engine.pipeline.completion.CompletionPipeline; +import me.whereareiam.identica.engine.pipeline.completion.lifecycle.CompletionPendingLifecycle; +import me.whereareiam.identica.engine.pipeline.completion.runtime.CompletionPipeline; import me.whereareiam.identica.event.EventManager; import me.whereareiam.identica.event.delivery.DeliveryCheckpointReachedEvent; import me.whereareiam.identica.event.routing.completion.CompletionRoutingReachedEvent; @@ -70,14 +70,12 @@ void sessionOpenedStoresPendingCompletion() { lifecycle.onSessionOpened(new SessionOpenedEvent( connectionUniqueId, PipelineType.MIGRATION, - session, - true + session )); verify(deliveryService).queue(argThat((DeliveryRequest request) -> request != null && request.getPayload().getCompletion() != null - && request.getPayload().getCompletion().isAuthenticationRecognized() && request.getCheckpoint() == DeliveryCheckpoint.PLATFORM_READY_INITIAL && "migration-limbo".equals(request.getRequiredServer()) )); @@ -112,8 +110,7 @@ void authenticationCompletionUsesInitialReadyCheckpoint() { lifecycle.onSessionOpened(new SessionOpenedEvent( connectionUniqueId, PipelineType.AUTHENTICATION, - session, - true + session )); verify(deliveryService).queue(argThat((DeliveryRequest request) -> @@ -200,7 +197,6 @@ void deliveryCheckpointDispatchesQueuedCompletion() { .connectionUniqueId(connectionUniqueId) .accountUniqueId(accountUniqueId) .pipelineType(PipelineType.MIGRATION) - .authenticationRecognized(true) .build()) .build()) .checkpoint(DeliveryCheckpoint.PLATFORM_READY_INITIAL) @@ -227,7 +223,6 @@ void deliveryCheckpointDispatchesQueuedCompletion() { && pendingState.getPipelineType() == PipelineType.MIGRATION && connectionUniqueId.equals(pendingState.getConnectionUniqueId()) && accountUniqueId.equals(pendingState.getAccountUniqueId()) - && pendingState.isAuthenticationRecognized() )); verify(deliveryService).acknowledge(deliveryId, "completion-dispatched"); } diff --git a/identica-engine/src/test/java/me/whereareiam/identica/engine/completion/CompletionPipelineTest.java b/identica-engine/src/test/java/me/whereareiam/identica/engine/completion/CompletionPipelineTest.java index 398a048cd..3a4e29b15 100644 --- a/identica-engine/src/test/java/me/whereareiam/identica/engine/completion/CompletionPipelineTest.java +++ b/identica-engine/src/test/java/me/whereareiam/identica/engine/completion/CompletionPipelineTest.java @@ -1,14 +1,14 @@ package me.whereareiam.identica.engine.completion; import me.whereareiam.identica.engine.pipeline.PipelineExecutor; -import me.whereareiam.identica.engine.pipeline.completion.CompletionPipeline; -import me.whereareiam.identica.engine.pipeline.completion.CompletionPipelineRegistry; import me.whereareiam.identica.engine.pipeline.completion.group.context.CompletionContextGroup; import me.whereareiam.identica.engine.pipeline.completion.group.context.phase.BuildCompletionContextPhase; import me.whereareiam.identica.engine.pipeline.completion.group.context.phase.ResolveCompletionProviderPhase; import me.whereareiam.identica.engine.pipeline.completion.group.context.phase.ResolveCompletionSessionPhase; import me.whereareiam.identica.engine.pipeline.completion.group.step.CompletionStepGroup; import me.whereareiam.identica.engine.pipeline.completion.group.step.phase.ExecuteCompletionStepsPhase; +import me.whereareiam.identica.engine.pipeline.completion.registry.CompletionPipelineRegistry; +import me.whereareiam.identica.engine.pipeline.completion.runtime.CompletionPipeline; import me.whereareiam.identica.identity.actor.Identity; import me.whereareiam.identica.identity.session.SessionService; import me.whereareiam.identica.model.Session; @@ -60,7 +60,6 @@ void consumeAndExecuteResolvesSessionAndProviderSteps() { .pipelineType(PipelineType.MIGRATION) .connectionUniqueId(connectionUniqueId) .accountUniqueId(accountUniqueId) - .authenticationRecognized(true) .build(); Session session = Session.builder() .uniqueId(accountUniqueId) @@ -91,7 +90,6 @@ void consumeAndExecuteResolvesSessionAndProviderSteps() { && context.getPipelineType() == PipelineType.MIGRATION && context.getSession().getProviderId().equals("credential") && context.getProvider() == provider - && context.isAuthenticationRecognized() )); } diff --git a/identica-engine/src/test/java/me/whereareiam/identica/engine/pipeline/DefaultPipelineStateStoreTest.java b/identica-engine/src/test/java/me/whereareiam/identica/engine/pipeline/DefaultPipelineStateStoreTest.java index 6b6cf6b47..218ac5620 100644 --- a/identica-engine/src/test/java/me/whereareiam/identica/engine/pipeline/DefaultPipelineStateStoreTest.java +++ b/identica-engine/src/test/java/me/whereareiam/identica/engine/pipeline/DefaultPipelineStateStoreTest.java @@ -4,8 +4,8 @@ import me.whereareiam.identica.event.EventManager; import me.whereareiam.identica.model.config.Replication; import me.whereareiam.identica.model.pipeline.journey.JourneyStateItem; -import me.whereareiam.identica.model.pipeline.state.PipelineState; -import me.whereareiam.identica.model.pipeline.state.PipelineStateReference; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.PipelineStateReference; import me.whereareiam.identica.replication.ReplicationAdapter; import me.whereareiam.identica.type.pipeline.PipelineType; import me.whereareiam.identica.util.EventUtil; diff --git a/identica-engine/src/test/java/me/whereareiam/identica/engine/pipeline/PendingPipelineKickCoordinatorTest.java b/identica-engine/src/test/java/me/whereareiam/identica/engine/pipeline/PendingPipelineKickCoordinatorTest.java index 7af06ad0b..a7a2196a0 100644 --- a/identica-engine/src/test/java/me/whereareiam/identica/engine/pipeline/PendingPipelineKickCoordinatorTest.java +++ b/identica-engine/src/test/java/me/whereareiam/identica/engine/pipeline/PendingPipelineKickCoordinatorTest.java @@ -2,9 +2,9 @@ import me.whereareiam.identica.Serializer; import me.whereareiam.identica.common.event.EventController; +import me.whereareiam.identica.engine.pipeline.scenario.PendingPipelineKickCoordinator; import me.whereareiam.identica.event.EventListener; import me.whereareiam.identica.event.base.IdenticEvent; -import me.whereareiam.identica.type.ScenarioResolution; import me.whereareiam.identica.event.scenario.authentication.AuthenticationRequiredEvent; import me.whereareiam.identica.event.scenario.authentication.AuthenticationResolvedEvent; import me.whereareiam.identica.event.scenario.migration.MigrationRequiredEvent; @@ -15,13 +15,10 @@ import me.whereareiam.identica.model.auth.AuthContext; import me.whereareiam.identica.model.config.Messages; import me.whereareiam.identica.model.migration.MigrationContext; -import me.whereareiam.identica.model.scheduler.DelayedRunnableTask; -import me.whereareiam.identica.model.scheduler.JobKey; -import me.whereareiam.identica.model.scheduler.Origin; -import me.whereareiam.identica.model.scheduler.PeriodicalRunnableTask; -import me.whereareiam.identica.model.scheduler.RunnableTask; +import me.whereareiam.identica.model.scheduler.*; import me.whereareiam.identica.service.DeliveryService; import me.whereareiam.identica.service.Scheduler; +import me.whereareiam.identica.type.ScenarioResolution; import me.whereareiam.identica.type.pipeline.journey.JourneyMode; import me.whereareiam.keystone.model.SerializerContent; import me.whereareiam.keystone.model.SerializerOptions; @@ -35,13 +32,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; +import java.util.*; import java.util.concurrent.atomic.AtomicReference; import static org.junit.jupiter.api.Assertions.assertFalse; diff --git a/identica-engine/src/test/java/me/whereareiam/identica/engine/pipeline/prepare/group/profile/phase/ResolvePendingMigrationAccountPhaseTest.java b/identica-engine/src/test/java/me/whereareiam/identica/engine/pipeline/prepare/group/profile/phase/ResolvePendingMigrationAccountPhaseTest.java new file mode 100644 index 000000000..1363c7ff7 --- /dev/null +++ b/identica-engine/src/test/java/me/whereareiam/identica/engine/pipeline/prepare/group/profile/phase/ResolvePendingMigrationAccountPhaseTest.java @@ -0,0 +1,105 @@ +package me.whereareiam.identica.engine.pipeline.prepare.group.profile.phase; + +import me.whereareiam.identica.database.AccountPersistenceService; +import me.whereareiam.identica.model.identity.Account; +import me.whereareiam.identica.model.migration.MigrationContext; +import me.whereareiam.identica.model.pipeline.journey.JourneyStateItem; +import me.whereareiam.identica.model.pipeline.migration.MigrationPendingState; +import me.whereareiam.identica.model.pipeline.prepare.PrepareAccountCandidateItem; +import me.whereareiam.identica.model.pipeline.prepare.PrepareContextItem; +import me.whereareiam.identica.model.pipeline.prepare.PrepareRequest; +import me.whereareiam.identica.model.provider.ProviderContext; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.PipelineStateReference; +import me.whereareiam.identica.pipeline.state.PipelineStateStore; +import me.whereareiam.identica.pipeline.state.prepare.PrepareGroupState; +import me.whereareiam.identica.service.DeliveryService; +import me.whereareiam.identica.type.PrepareStage; +import me.whereareiam.identica.type.migration.MigrationInitiator; +import me.whereareiam.identica.type.pipeline.PipelineType; +import me.whereareiam.identica.type.provider.ProviderOrigin; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.Duration; +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@DisplayName("Resolve Pending Migration Account Phase") +class ResolvePendingMigrationAccountPhaseTest { + @Mock + private PipelineStateStore pipelineStateStore; + @Mock + private AccountPersistenceService accountPersistenceService; + @Mock + private DeliveryService deliveryService; + + @DisplayName("Restores the pending migration account without requiring provider subject") + @Test + void restoresPendingMigrationAccountWithoutProviderSubject() { + UUID connectionUniqueId = UUID.randomUUID(); + UUID accountUniqueId = UUID.randomUUID(); + String connectionKey = "PlayerOne:127.0.0.1"; + + PipelineState pendingState = PipelineState.initial(); + pendingState.setPipelineType(PipelineType.MIGRATION); + pendingState.setScenario(MigrationContext.builder() + .connectionUniqueId(connectionUniqueId) + .identity(new me.whereareiam.identica.identity.actor.ConnectionIdentity(accountUniqueId, "PlayerOne", "127.0.0.1")) + .targetProviderId("credential") + .build()); + pendingState.putItem(new MigrationPendingState( + "credential", + 1234L, + MigrationInitiator.USER, + connectionUniqueId + ), Duration.ofMinutes(5).toMillis()); + pendingState.putItem(new JourneyStateItem(null, null, 0), Duration.ofMinutes(5).toMillis()); + + doReturn(Optional.of(pendingState)).when(pipelineStateStore).find(any(PipelineStateReference.class)); + when(accountPersistenceService.findByUniqueId(accountUniqueId)).thenReturn(Optional.of(Account.builder() + .uniqueId(accountUniqueId) + .username("PlayerOne") + .build())); + + ResolvePendingMigrationAccountPhase phase = new ResolvePendingMigrationAccountPhase( + pipelineStateStore, + accountPersistenceService, + deliveryService + ); + + PipelineState state = PipelineState.initial(); + state.putItem(new PrepareContextItem(ProviderContext.of( + "credential", + null, + "PlayerOne", + ProviderOrigin.MANUAL + ), null, null), 0L); + + PrepareGroupState groupState = new PrepareGroupState(); + groupState.setRequest(PrepareRequest.builder() + .connectionKey(connectionKey) + .stage(PrepareStage.PROFILE) + .identity(new me.whereareiam.identica.identity.actor.ConnectionIdentity("PlayerOne", "127.0.0.1")) + .build()); + + phase.execute(state, groupState).toCompletableFuture().join(); + + PrepareAccountCandidateItem candidate = state.item(PrepareAccountCandidateItem.class).orElse(null); + assertNotNull(candidate); + assertEquals(accountUniqueId, candidate.getUniqueId()); + assertNotNull(candidate.getAccount()); + assertEquals("PlayerOne", candidate.getAccount().getUsername()); + assertNull(candidate.getLink()); + assertNull(candidate.getProfile()); + } +} diff --git a/identica-engine/src/test/java/me/whereareiam/identica/engine/pipeline/scenario/ScenarioPipelineResumeContextTest.java b/identica-engine/src/test/java/me/whereareiam/identica/engine/pipeline/scenario/ScenarioPipelineResumeContextTest.java index fea22fe60..a748c3162 100644 --- a/identica-engine/src/test/java/me/whereareiam/identica/engine/pipeline/scenario/ScenarioPipelineResumeContextTest.java +++ b/identica-engine/src/test/java/me/whereareiam/identica/engine/pipeline/scenario/ScenarioPipelineResumeContextTest.java @@ -2,20 +2,20 @@ import me.whereareiam.identica.database.provider.ProviderLinkPersistenceService; import me.whereareiam.identica.engine.pipeline.PipelineExecutor; -import me.whereareiam.identica.engine.pipeline.scenario.authentication.AuthenticationPipeline; -import me.whereareiam.identica.engine.pipeline.scenario.migration.MigrationPipeline; -import me.whereareiam.identica.engine.pipeline.scenario.registration.RegistrationPipeline; +import me.whereareiam.identica.engine.pipeline.scenario.type.authentication.AuthenticationPipeline; +import me.whereareiam.identica.engine.pipeline.scenario.type.migration.MigrationPipeline; +import me.whereareiam.identica.engine.pipeline.scenario.type.registration.RegistrationPipeline; import me.whereareiam.identica.identity.actor.ConnectionIdentity; import me.whereareiam.identica.model.auth.AuthContext; import me.whereareiam.identica.model.auth.request.ResumeRequest; import me.whereareiam.identica.model.config.Engine; import me.whereareiam.identica.model.config.Messages; import me.whereareiam.identica.model.migration.MigrationContext; -import me.whereareiam.identica.model.pipeline.state.PipelineState; -import me.whereareiam.identica.model.pipeline.state.PipelineStateReference; import me.whereareiam.identica.model.registration.RegistrationContext; import me.whereareiam.identica.pipeline.PipelineRegistry; import me.whereareiam.identica.pipeline.ScenarioContext; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.PipelineStateReference; import me.whereareiam.identica.pipeline.state.PipelineStateStore; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/identica-engine/src/test/java/me/whereareiam/identica/engine/pipeline/scenario/base/journey/phase/ExecutePlanPhaseTest.java b/identica-engine/src/test/java/me/whereareiam/identica/engine/pipeline/scenario/base/journey/phase/ExecutePlanPhaseTest.java index bd8819971..dc9b68c07 100644 --- a/identica-engine/src/test/java/me/whereareiam/identica/engine/pipeline/scenario/base/journey/phase/ExecutePlanPhaseTest.java +++ b/identica-engine/src/test/java/me/whereareiam/identica/engine/pipeline/scenario/base/journey/phase/ExecutePlanPhaseTest.java @@ -1,14 +1,12 @@ package me.whereareiam.identica.engine.pipeline.scenario.base.journey.phase; -import me.whereareiam.identica.engine.pipeline.scenario.base.journey.JourneyState; +import me.whereareiam.identica.engine.pipeline.scenario.shared.group.journey.phase.ExecutePlanPhase; import me.whereareiam.identica.event.EventManager; import me.whereareiam.identica.identity.IdentityService; import me.whereareiam.identica.identity.actor.ConnectionIdentity; import me.whereareiam.identica.model.auth.AuthContext; -import me.whereareiam.identica.model.config.Messages; import me.whereareiam.identica.model.config.Engine; -import me.whereareiam.identica.model.pipeline.authentication.AuthenticationOutcomeItem; -import me.whereareiam.identica.model.pipeline.authentication.AuthenticationOutcomeItem.AuthenticationOutcome; +import me.whereareiam.identica.model.config.Messages; import me.whereareiam.identica.model.pipeline.journey.JourneyStateItem; import me.whereareiam.identica.model.pipeline.journey.execution.JourneyExecutionBlock; import me.whereareiam.identica.model.pipeline.journey.execution.JourneyExecutionPlan; @@ -17,12 +15,12 @@ import me.whereareiam.identica.model.pipeline.journey.stage.step.JourneyStep; import me.whereareiam.identica.model.pipeline.journey.stage.step.StepResult; import me.whereareiam.identica.model.pipeline.phase.PhaseResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; -import me.whereareiam.identica.model.pipeline.state.PipelineStateReference; import me.whereareiam.identica.pipeline.ScenarioContext; import me.whereareiam.identica.pipeline.journey.step.Step; -import me.whereareiam.identica.pipeline.journey.step.type.AuthenticationRecognitionStep; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.PipelineStateReference; import me.whereareiam.identica.pipeline.state.PipelineStateStore; +import me.whereareiam.identica.pipeline.state.scenario.shared.JourneyState; import me.whereareiam.identica.provider.ProviderManager; import me.whereareiam.identica.routing.RoutingCoordinator; import me.whereareiam.identica.type.pipeline.PipelineStatus; @@ -58,7 +56,7 @@ void completeStepStopsCurrentStageOnly() { ProviderManager providerManager = mock(ProviderManager.class); PipelineStateStore pipelineStateStore = mock(PipelineStateStore.class); RoutingCoordinator routingCoordinator = mock(RoutingCoordinator.class); - when(pipelineStateStore.find(any(me.whereareiam.identica.model.pipeline.state.PipelineStateReference.class))) + when(pipelineStateStore.find(any(me.whereareiam.identica.pipeline.state.PipelineStateReference.class))) .thenReturn(Optional.empty()); ExecutePlanPhase phase = new ExecutePlanPhase( @@ -149,70 +147,6 @@ void completeStepStopsCurrentStageOnly() { verify(routingCoordinator, atLeastOnce()).accept(any()); } - @DisplayName("Completed recognition steps store an authentication outcome item during journey execution") - @Test - void recognitionCompletionStoresAuthenticationOutcome() { - IdentityService identityService = mock(IdentityService.class); - EventManager eventManager = mock(EventManager.class); - ProviderManager providerManager = mock(ProviderManager.class); - PipelineStateStore pipelineStateStore = mock(PipelineStateStore.class); - RoutingCoordinator routingCoordinator = mock(RoutingCoordinator.class); - when(pipelineStateStore.find(any(PipelineStateReference.class))).thenReturn(Optional.empty()); - - ExecutePlanPhase phase = new ExecutePlanPhase( - identityService, - eventManager, - this::settings, - Messages::new, - providerManager, - pipelineStateStore, - routingCoordinator - ); - - Step contributeOutcome = new RecognitionStep(); - - JourneyStage providerStage = JourneyStage.builder() - .id(StageType.PROVIDER.id()) - .type(StageType.PROVIDER) - .order(200) - .pipelineTypes(EnumSet.of(PipelineType.AUTHENTICATION)) - .journeyModes(EnumSet.of(JourneyMode.INTERACTIVE)) - .requireCompletion(true) - .allowFallback(true) - .build(); - - JourneyExecutionPlan plan = new JourneyExecutionPlan(List.of( - new JourneyExecutionBlock( - "group-provider", - JourneyExecutionPolicy.SEQUENTIAL, - null, - List.of(new JourneyExecutionStage(providerStage, List.of( - journeyStep(StageType.PROVIDER, contributeOutcome, 10) - ))) - ) - )); - - AuthContext context = AuthContext.builder() - .connectionUniqueId(UUID.randomUUID()) - .identity(new ConnectionIdentity(UUID.randomUUID(), "PlayerOne", "127.0.0.1")) - .intendedServer("auth") - .build(); - PipelineState pipelineState = PipelineState.initial(); - pipelineState.setPipelineType(PipelineType.AUTHENTICATION); - pipelineState.setScenario(context); - - JourneyState state = new JourneyState(); - state.setContext(context); - state.setJourneyMode(JourneyMode.INTERACTIVE); - state.setExecutionPlan(plan); - - PhaseResult result = phase.execute(pipelineState, state).toCompletableFuture().join(); - - assertEquals(PipelineStatus.COMPLETE, result.getState().getResult().getStatus()); - assertEquals(AuthenticationOutcome.RECOGNIZED, - pipelineState.item(AuthenticationOutcomeItem.class).orElseThrow().getOutcome()); - } - @DisplayName("Waiting steps persist the current scenario identity reference") @Test void waitingStepPersistsCurrentScenarioIdentityReference() { @@ -346,25 +280,4 @@ private interface StepExecutor { @NotNull StepResult execute(@NotNull ScenarioContext context); } - private static final class RecognitionStep implements AuthenticationRecognitionStep { - @Override - public @NotNull String getName() { - return "recognize"; - } - - @Override - public @NotNull java.util.Set journeyModes() { - return EnumSet.of(JourneyMode.INTERACTIVE); - } - - @Override - public @NotNull StepContextRequirement contextRequirement() { - return StepContextRequirement.LOGIN; - } - - @Override - public @NotNull CompletableFuture execute(@NotNull ScenarioContext context) { - return CompletableFuture.completedFuture(StepResult.complete(context)); - } - } } diff --git a/identica-engine/src/test/java/me/whereareiam/identica/engine/pipeline/scenario/migration/group/identity/phase/ApplyProviderLinkPhaseTest.java b/identica-engine/src/test/java/me/whereareiam/identica/engine/pipeline/scenario/migration/group/identity/phase/ApplyProviderLinkPhaseTest.java index 3fad40e83..0afbf5ef0 100644 --- a/identica-engine/src/test/java/me/whereareiam/identica/engine/pipeline/scenario/migration/group/identity/phase/ApplyProviderLinkPhaseTest.java +++ b/identica-engine/src/test/java/me/whereareiam/identica/engine/pipeline/scenario/migration/group/identity/phase/ApplyProviderLinkPhaseTest.java @@ -2,13 +2,13 @@ import me.whereareiam.identica.database.provider.ProviderLinkPersistenceService; import me.whereareiam.identica.database.provider.ProviderProfilePersistenceService; -import me.whereareiam.identica.engine.pipeline.scenario.migration.group.identity.IdentityState; +import me.whereareiam.identica.engine.pipeline.scenario.type.migration.group.identity.phase.ApplyProviderLinkPhase; import me.whereareiam.identica.model.identity.Account; import me.whereareiam.identica.model.identity.provider.AccountProviderLink; import me.whereareiam.identica.model.identity.provider.AccountProviderProfile; import me.whereareiam.identica.model.pipeline.PipelineResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; -import me.whereareiam.identica.type.UsernameSource; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.type.migration.IdentityState; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -40,7 +40,6 @@ void linksPremiumProviderToExistingAccountUuid() { Account account = Account.builder() .uniqueId(existingAccountUniqueId) .username("MigratedPlayer") - .source(UsernameSource.PROVIDER) .build(); AccountProviderProfile profile = AccountProviderProfile.builder() .providerId("premium") diff --git a/identica-engine/src/test/java/me/whereareiam/identica/engine/pipeline/scenario/session/OpenSessionCompletionPendingTest.java b/identica-engine/src/test/java/me/whereareiam/identica/engine/pipeline/scenario/session/OpenSessionCompletionPendingTest.java index dd9d6f8db..be40338e3 100644 --- a/identica-engine/src/test/java/me/whereareiam/identica/engine/pipeline/scenario/session/OpenSessionCompletionPendingTest.java +++ b/identica-engine/src/test/java/me/whereareiam/identica/engine/pipeline/scenario/session/OpenSessionCompletionPendingTest.java @@ -1,6 +1,6 @@ package me.whereareiam.identica.engine.pipeline.scenario.session; -import me.whereareiam.identica.engine.pipeline.scenario.authentication.group.session.phase.OpenSessionPhase; +import me.whereareiam.identica.engine.pipeline.scenario.type.authentication.group.session.phase.OpenSessionPhase; import me.whereareiam.identica.event.EventManager; import me.whereareiam.identica.event.session.SessionOpenedEvent; import me.whereareiam.identica.identity.actor.ConnectionIdentity; @@ -9,9 +9,8 @@ import me.whereareiam.identica.model.auth.AuthContext; import me.whereareiam.identica.model.config.Messages; import me.whereareiam.identica.model.pipeline.PipelineResult; -import me.whereareiam.identica.model.pipeline.authentication.AuthenticationOutcomeItem; -import me.whereareiam.identica.model.pipeline.authentication.AuthenticationOutcomeItem.AuthenticationOutcome; -import me.whereareiam.identica.model.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.scenario.type.authentication.SessionState; import me.whereareiam.identica.type.pipeline.PipelineType; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -50,14 +49,13 @@ void authenticationOpenSessionStoresPendingCompletionInvocation() { .originalUsername("PlayerOne") .effectiveUsername("PlayerOne") .build(); - me.whereareiam.identica.engine.pipeline.scenario.authentication.group.session.SessionState state = - new me.whereareiam.identica.engine.pipeline.scenario.authentication.group.session.SessionState(); + + SessionState state = new SessionState(); state.setAuthContext(context); state.setSession(session); state.setResult(PipelineResult.complete()); PipelineState pipelineState = PipelineState.initial(); pipelineState.setPipelineType(PipelineType.AUTHENTICATION); - pipelineState.putItem(new AuthenticationOutcomeItem(AuthenticationOutcome.RECOGNIZED), 0L); when(sessionService.open(session)) .thenReturn(CompletableFuture.completedFuture(session)); @@ -69,7 +67,6 @@ void authenticationOpenSessionStoresPendingCompletionInvocation() { && requested.getPipelineType() == PipelineType.AUTHENTICATION && accountUniqueId.equals(requested.getSession().getUniqueId()) && "credential".equals(requested.getSession().getProviderId()) - && requested.isAuthenticationRecognized() )); } diff --git a/identica-engine/src/test/java/me/whereareiam/identica/engine/pipeline/scenario/shared/group/journey/rule/SelectProvidersRuleTest.java b/identica-engine/src/test/java/me/whereareiam/identica/engine/pipeline/scenario/shared/group/journey/rule/SelectProvidersRuleTest.java new file mode 100644 index 000000000..8a7f4332a --- /dev/null +++ b/identica-engine/src/test/java/me/whereareiam/identica/engine/pipeline/scenario/shared/group/journey/rule/SelectProvidersRuleTest.java @@ -0,0 +1,146 @@ +package me.whereareiam.identica.engine.pipeline.scenario.shared.group.journey.rule; + +import me.whereareiam.identica.database.provider.ProviderLinkPersistenceService; +import me.whereareiam.identica.identity.actor.ConnectionIdentity; +import me.whereareiam.identica.model.migration.MigrationContext; +import me.whereareiam.identica.model.pipeline.journey.JourneyPlan; +import me.whereareiam.identica.model.pipeline.journey.JourneyRuleContext; +import me.whereareiam.identica.model.pipeline.journey.execution.JourneyExecutionBlock; +import me.whereareiam.identica.model.pipeline.journey.execution.JourneyExecutionPlan; +import me.whereareiam.identica.model.pipeline.journey.execution.JourneyExecutionStage; +import me.whereareiam.identica.model.pipeline.journey.stage.JourneyStage; +import me.whereareiam.identica.model.pipeline.journey.stage.step.JourneyStep; +import me.whereareiam.identica.model.pipeline.journey.stage.step.StepResult; +import me.whereareiam.identica.model.provider.InternalProvider; +import me.whereareiam.identica.model.provider.ProviderDescriptor; +import me.whereareiam.identica.pipeline.journey.registry.type.AuthenticationJourneyRegistry; +import me.whereareiam.identica.pipeline.journey.registry.type.MigrationJourneyRegistry; +import me.whereareiam.identica.pipeline.journey.registry.type.RegistrationJourneyRegistry; +import me.whereareiam.identica.pipeline.journey.step.Step; +import me.whereareiam.identica.pipeline.state.PipelineStateStore; +import me.whereareiam.identica.provider.ProviderOperations; +import me.whereareiam.identica.type.pipeline.PipelineType; +import me.whereareiam.identica.type.pipeline.journey.JourneyExecutionPolicy; +import me.whereareiam.identica.type.pipeline.journey.JourneyMode; +import me.whereareiam.identica.type.pipeline.journey.StageType; +import me.whereareiam.identica.type.pipeline.journey.step.StepContextRequirement; +import me.whereareiam.identica.type.provider.ProviderState; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.EnumSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@DisplayName("Select Providers Rule") +class SelectProvidersRuleTest { + @Test + @DisplayName("Migration selection uses the target provider plan without capability metadata") + void migrationSelectionUsesTargetProviderPlan() { + ProviderOperations providerOperations = mock(ProviderOperations.class); + AuthenticationJourneyRegistry authenticationJourneyRegistry = mock(AuthenticationJourneyRegistry.class); + RegistrationJourneyRegistry registrationJourneyRegistry = mock(RegistrationJourneyRegistry.class); + MigrationJourneyRegistry migrationJourneyRegistry = mock(MigrationJourneyRegistry.class); + ProviderLinkPersistenceService providerLinkPersistenceService = mock(ProviderLinkPersistenceService.class); + PipelineStateStore pipelineStateStore = mock(PipelineStateStore.class); + + InternalProvider provider = provider("premium"); + MigrationContext context = MigrationContext.builder() + .connectionUniqueId(UUID.randomUUID()) + .identity(new ConnectionIdentity(UUID.randomUUID(), "PlayerOne", "127.0.0.1")) + .targetProviderId("premium") + .build(); + when(providerOperations.eligibleProviders(context, PipelineType.MIGRATION, JourneyMode.SEAMLESS)) + .thenReturn(List.of(provider)); + when(migrationJourneyRegistry.resolvePlan(context, PipelineType.MIGRATION, JourneyMode.SEAMLESS, "premium")) + .thenReturn(providerPlan("premium")); + + SelectProvidersRule rule = new SelectProvidersRule( + providerOperations, + authenticationJourneyRegistry, + registrationJourneyRegistry, + migrationJourneyRegistry, + providerLinkPersistenceService, + pipelineStateStore + ); + + JourneyExecutionPlan result = rule.apply( + new JourneyRuleContext(context, PipelineType.MIGRATION, JourneyMode.SEAMLESS, null, null), + seedPlan() + ); + + assertEquals(1, result.blocks().size()); + assertEquals("premium", result.blocks().getFirst().providerId()); + assertEquals(1, result.blocks().getFirst().stages().size()); + } + + private InternalProvider provider(String id) { + ProviderDescriptor descriptor = new ProviderDescriptor(); + descriptor.setId(id); + descriptor.setName(id); + descriptor.setVersion("1.0.0"); + descriptor.setMain("ignored.Main"); + descriptor.setSupportedPlatforms(List.of("ANY")); + return InternalProvider.builder().descriptor(descriptor).state(ProviderState.ENABLED).build(); + } + + private JourneyExecutionPlan seedPlan() { + JourneyStage stage = providerStage(); + return new JourneyExecutionPlan(List.of(new JourneyExecutionBlock( + "seed", + JourneyExecutionPolicy.SEQUENTIAL, + null, + List.of(new JourneyExecutionStage(stage, List.of())) + ))); + } + + private JourneyPlan providerPlan(String providerId) { + return new JourneyPlan(List.of(new JourneyPlan.StageEntry( + providerStage(), + List.of(JourneyStep.builder() + .stageId(StageType.PROVIDER.id()) + .providerId(providerId) + .scenarios(EnumSet.of(PipelineType.MIGRATION)) + .journeyModes(EnumSet.of(JourneyMode.SEAMLESS)) + .step(new Step() { + @Override + public @NotNull String getName() { + return "migration-step"; + } + + @Override + public @NotNull Set journeyModes() { + return Set.of(JourneyMode.SEAMLESS); + } + + @Override + public @NotNull StepContextRequirement contextRequirement() { + return StepContextRequirement.LOGIN; + } + + @Override + public @NotNull CompletableFuture execute(@NotNull me.whereareiam.identica.pipeline.ScenarioContext context) { + return CompletableFuture.completedFuture(null); + } + }) + .build()) + ))); + } + + private JourneyStage providerStage() { + return JourneyStage.builder() + .id(StageType.PROVIDER.id()) + .type(StageType.PROVIDER) + .order(100) + .pipelineTypes(EnumSet.of(PipelineType.MIGRATION)) + .journeyModes(EnumSet.allOf(JourneyMode.class)) + .build(); + } +} diff --git a/identica-engine/src/test/java/me/whereareiam/identica/engine/pipeline/scenario/type/authentication/AuthenticationPipelineTest.java b/identica-engine/src/test/java/me/whereareiam/identica/engine/pipeline/scenario/type/authentication/AuthenticationPipelineTest.java new file mode 100644 index 000000000..7951a849b --- /dev/null +++ b/identica-engine/src/test/java/me/whereareiam/identica/engine/pipeline/scenario/type/authentication/AuthenticationPipelineTest.java @@ -0,0 +1,47 @@ +package me.whereareiam.identica.engine.pipeline.scenario.type.authentication; + +import me.whereareiam.identica.database.provider.ProviderLinkPersistenceService; +import me.whereareiam.identica.engine.pipeline.PipelineExecutor; +import me.whereareiam.identica.model.auth.request.ConnectionRequest; +import me.whereareiam.identica.pipeline.PipelineRegistry; +import me.whereareiam.identica.pipeline.ScenarioContext; +import me.whereareiam.identica.pipeline.state.PipelineStateStore; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.mock; + +@DisplayName("Authentication Pipeline") +class AuthenticationPipelineTest { + @DisplayName("Fails to build a context when the request has no account UUID") + @Test + void buildContextRequiresPreparedAccountUniqueId() { + TestAuthenticationPipeline pipeline = new TestAuthenticationPipeline(); + + ScenarioContext context = pipeline.buildFor(ConnectionRequest.builder() + .identity(new me.whereareiam.identica.identity.actor.ConnectionIdentity("PlayerOne", "127.0.0.1")) + .build()); + + assertNull(context); + } + + private static final class TestAuthenticationPipeline extends AuthenticationPipeline { + private TestAuthenticationPipeline() { + super( + mock(PipelineRegistry.class), + me.whereareiam.identica.model.config.Messages::new, + me.whereareiam.identica.model.config.Engine::new, + mock(PipelineStateStore.class), + mock(PipelineExecutor.class), + mock(ProviderLinkPersistenceService.class) + ); + } + + private @Nullable ScenarioContext buildFor(@NotNull ConnectionRequest request) { + return buildContext(request); + } + } +} diff --git a/identica-engine/src/test/java/me/whereareiam/identica/engine/pipeline/scenario/type/migration/MigrationPipelineTest.java b/identica-engine/src/test/java/me/whereareiam/identica/engine/pipeline/scenario/type/migration/MigrationPipelineTest.java new file mode 100644 index 000000000..123106c1f --- /dev/null +++ b/identica-engine/src/test/java/me/whereareiam/identica/engine/pipeline/scenario/type/migration/MigrationPipelineTest.java @@ -0,0 +1,45 @@ +package me.whereareiam.identica.engine.pipeline.scenario.type.migration; + +import me.whereareiam.identica.engine.pipeline.PipelineExecutor; +import me.whereareiam.identica.model.auth.request.ConnectionRequest; +import me.whereareiam.identica.pipeline.PipelineRegistry; +import me.whereareiam.identica.pipeline.ScenarioContext; +import me.whereareiam.identica.pipeline.state.PipelineStateStore; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.mock; + +@DisplayName("Migration Pipeline") +class MigrationPipelineTest { + @DisplayName("Fails to build a context when the request has no account UUID") + @Test + void buildContextRequiresPreparedAccountUniqueId() { + TestMigrationPipeline pipeline = new TestMigrationPipeline(); + + ScenarioContext context = pipeline.buildFor(ConnectionRequest.builder() + .identity(new me.whereareiam.identica.identity.actor.ConnectionIdentity("PlayerOne", "127.0.0.1")) + .build()); + + assertNull(context); + } + + private static final class TestMigrationPipeline extends MigrationPipeline { + private TestMigrationPipeline() { + super( + mock(PipelineRegistry.class), + me.whereareiam.identica.model.config.Messages::new, + me.whereareiam.identica.model.config.Engine::new, + mock(PipelineStateStore.class), + mock(PipelineExecutor.class) + ); + } + + private @Nullable ScenarioContext buildFor(@NotNull ConnectionRequest request) { + return buildContext(request); + } + } +} diff --git a/identica-engine/src/test/java/me/whereareiam/identica/engine/pipeline/scenario/type/registration/RegistrationPipelineTest.java b/identica-engine/src/test/java/me/whereareiam/identica/engine/pipeline/scenario/type/registration/RegistrationPipelineTest.java new file mode 100644 index 000000000..cf369abcf --- /dev/null +++ b/identica-engine/src/test/java/me/whereareiam/identica/engine/pipeline/scenario/type/registration/RegistrationPipelineTest.java @@ -0,0 +1,47 @@ +package me.whereareiam.identica.engine.pipeline.scenario.type.registration; + +import me.whereareiam.identica.database.provider.ProviderLinkPersistenceService; +import me.whereareiam.identica.engine.pipeline.PipelineExecutor; +import me.whereareiam.identica.model.auth.request.ConnectionRequest; +import me.whereareiam.identica.pipeline.PipelineRegistry; +import me.whereareiam.identica.pipeline.ScenarioContext; +import me.whereareiam.identica.pipeline.state.PipelineStateStore; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.mock; + +@DisplayName("Registration Pipeline") +class RegistrationPipelineTest { + @DisplayName("Fails to build a context when the request has no account UUID") + @Test + void buildContextRequiresPreparedAccountUniqueId() { + TestRegistrationPipeline pipeline = new TestRegistrationPipeline(); + + ScenarioContext context = pipeline.buildFor(ConnectionRequest.builder() + .identity(new me.whereareiam.identica.identity.actor.ConnectionIdentity("PlayerOne", "127.0.0.1")) + .build()); + + assertNull(context); + } + + private static final class TestRegistrationPipeline extends RegistrationPipeline { + private TestRegistrationPipeline() { + super( + mock(PipelineRegistry.class), + me.whereareiam.identica.model.config.Messages::new, + me.whereareiam.identica.model.config.Engine::new, + mock(PipelineStateStore.class), + mock(PipelineExecutor.class), + mock(ProviderLinkPersistenceService.class) + ); + } + + private @Nullable ScenarioContext buildFor(@NotNull ConnectionRequest request) { + return buildContext(request); + } + } +} diff --git a/identica-engine/src/test/java/me/whereareiam/identica/engine/prepare/PreparePipelineTest.java b/identica-engine/src/test/java/me/whereareiam/identica/engine/prepare/PreparePipelineTest.java deleted file mode 100644 index 0f02c7ce1..000000000 --- a/identica-engine/src/test/java/me/whereareiam/identica/engine/prepare/PreparePipelineTest.java +++ /dev/null @@ -1,888 +0,0 @@ -package me.whereareiam.identica.engine.prepare; - -import me.whereareiam.identica.database.AccountPersistenceService; -import me.whereareiam.identica.database.provider.ProviderLinkPersistenceService; -import me.whereareiam.identica.database.provider.ProviderProfilePersistenceService; -import me.whereareiam.identica.engine.pipeline.PipelineExecutor; -import me.whereareiam.identica.engine.pipeline.prepare.ConnectionProviderContextResolver; -import me.whereareiam.identica.engine.pipeline.prepare.PreparePipeline; -import me.whereareiam.identica.engine.pipeline.prepare.PreparePipelineRegistry; -import me.whereareiam.identica.engine.pipeline.prepare.group.context.ContextGroup; -import me.whereareiam.identica.engine.pipeline.prepare.group.context.phase.ResolveEntrypointPhase; -import me.whereareiam.identica.engine.pipeline.prepare.group.context.phase.ResolvePendingMigrationContextPhase; -import me.whereareiam.identica.engine.pipeline.prepare.group.context.phase.RestorePrepareStatePhase; -import me.whereareiam.identica.engine.pipeline.prepare.group.finalize.FinalizeGroup; -import me.whereareiam.identica.engine.pipeline.prepare.group.finalize.phase.StorePrepareDecisionPhase; -import me.whereareiam.identica.engine.pipeline.prepare.group.handshake.HandshakeGroup; -import me.whereareiam.identica.engine.pipeline.prepare.group.handshake.phase.EvaluateHandshakePhase; -import me.whereareiam.identica.engine.pipeline.prepare.group.handshake.phase.FinalizeHandshakePhase; -import me.whereareiam.identica.engine.pipeline.prepare.group.policy.PolicyGroup; -import me.whereareiam.identica.engine.pipeline.prepare.group.policy.phase.ApplyPreparePolicyPhase; -import me.whereareiam.identica.engine.pipeline.prepare.group.policy.phase.ApplyProviderJoinRestrictionPhase; -import me.whereareiam.identica.engine.pipeline.prepare.group.profile.ProfileGroup; -import me.whereareiam.identica.engine.pipeline.prepare.group.profile.phase.LoadPrepareAccountPhase; -import me.whereareiam.identica.engine.pipeline.prepare.group.profile.phase.ResolvePendingMigrationAccountPhase; -import me.whereareiam.identica.engine.pipeline.prepare.group.profile.phase.ResolvePreparedAccountPhase; -import me.whereareiam.identica.engine.pipeline.prepare.group.profile.phase.ResolveProfilePhase; -import me.whereareiam.identica.event.EventManager; -import me.whereareiam.identica.event.account.AccountPrepareEvent; -import me.whereareiam.identica.event.base.Event; -import me.whereareiam.identica.handshake.HandshakeStore; -import me.whereareiam.identica.handshake.policy.HandshakePolicy; -import me.whereareiam.identica.handshake.policy.ProviderScopedHandshakePolicy; -import me.whereareiam.identica.identity.account.RegistrationAccountService; -import me.whereareiam.identica.identity.actor.ConnectionIdentity; -import me.whereareiam.identica.identity.session.recognition.eligibility.RecognitionEligibilityService; -import me.whereareiam.identica.model.auth.handshake.HandshakeDecision; -import me.whereareiam.identica.model.auth.handshake.HandshakeRequest; -import me.whereareiam.identica.model.config.Messages; -import me.whereareiam.identica.model.identity.Account; -import me.whereareiam.identica.model.identity.AccountDecision; -import me.whereareiam.identica.model.identity.provider.AccountProviderLink; -import me.whereareiam.identica.model.identity.provider.AccountProviderProfile; -import me.whereareiam.identica.model.migration.MigrationContext; -import me.whereareiam.identica.model.pipeline.migration.MigrationPendingState; -import me.whereareiam.identica.model.pipeline.prepare.PrepareRequest; -import me.whereareiam.identica.model.pipeline.prepare.decision.PrepareDecision; -import me.whereareiam.identica.model.pipeline.state.PipelineState; -import me.whereareiam.identica.model.pipeline.state.PipelineStateReference; -import me.whereareiam.identica.model.provider.ProviderContext; -import me.whereareiam.identica.model.provider.restriction.ProviderJoinRestrictionDecision; -import me.whereareiam.identica.model.provider.ResolvedEntrypoint; -import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityDecision; -import me.whereareiam.identica.pipeline.prepare.PrepareStateStore; -import me.whereareiam.identica.pipeline.state.PipelineStateStore; -import me.whereareiam.identica.provider.restriction.ProviderJoinRestrictionService; -import me.whereareiam.identica.provider.ProviderOperations; -import me.whereareiam.identica.provider.profile.ProfileResolution; -import me.whereareiam.identica.service.DeliveryService; -import me.whereareiam.identica.type.PrepareStage; -import me.whereareiam.identica.type.UsernameSource; -import me.whereareiam.identica.type.migration.MigrationInitiator; -import me.whereareiam.identica.type.pipeline.PipelineType; -import me.whereareiam.identica.type.provider.ProviderOrigin; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -@DisplayName("Prepare Pipeline") -class PreparePipelineTest { - @Mock - private HandshakeStore handshakeStore; - @Mock - private RegistrationAccountService registrationAccountService; - @Mock - private ProviderOperations providerOperations; - @Mock - private AccountPersistenceService accountPersistenceService; - @Mock - private ProviderLinkPersistenceService providerLinkPersistenceService; - @Mock - private ProviderProfilePersistenceService providerProfilePersistenceService; - @Mock - private EventManager eventManager; - @Mock - private PipelineStateStore pipelineStateStore; - @Mock - private RecognitionEligibilityService recognitionEligibilityService; - @Mock - private DeliveryService deliveryService; - @Mock - private ProviderJoinRestrictionService providerJoinRestrictionService; - - @BeforeEach - void setUpProviderRestrictionService() { - ProviderJoinRestrictionDecision allowed = ProviderJoinRestrictionDecision.builder() - .allowed(true) - .configured(false) - .active(false) - .allow(java.util.Set.of()) - .matchedConditions(java.util.Set.of()) - .build(); - lenient().when(providerJoinRestrictionService.evaluate(anyString())).thenReturn(allowed); - lenient().when(providerJoinRestrictionService.evaluate(anyString(), any(), any(), any(), any())).thenReturn(allowed); - } - - @DisplayName("Profile preparation builds a transient account and applies the event decision") - @Test - void profileStageBuildsTransientAccountAndHonorsPrepareDecision() { - UUID accountUniqueId = UUID.randomUUID(); - ConnectionIdentity identity = identity("PlayerOne"); - TestPrepareStateStore prepareStateStore = new TestPrepareStateStore(); - PreparePipeline pipeline = pipeline(prepareStateStore); - String connectionKey = "PlayerOne|127.0.0.1|premium.example.com|25565"; - identity.setObservedUniqueId(UUID.randomUUID()); - - when(handshakeStore.policies()).thenReturn(java.util.Set.of()); - when(providerOperations.resolveProfile(any())) - .thenReturn(ProfileResolution.builder() - .providerId("premium") - .providerSubject("premium-subject") - .build()); - when(providerOperations.resolveEntrypoint("premium.example.com", 25565)) - .thenReturn(ResolvedEntrypoint.builder() - .providerId("premium") - .host("premium.example.com") - .port(25565) - .build()); - when(registrationAccountService.reserve(any())).thenReturn(accountUniqueId); - when(providerLinkPersistenceService.findBySubject("premium", "premium-subject")) - .thenReturn(Optional.empty()); - when(accountPersistenceService.findByUniqueId(accountUniqueId)) - .thenReturn(Optional.empty()); - when(providerProfilePersistenceService.findBySubject("premium", "premium-subject")) - .thenReturn(Optional.empty()); - doAnswer(invocation -> { - Event event = invocation.getArgument(0); - if (event instanceof AccountPrepareEvent prepareEvent) { - prepareEvent.setEffectiveUsername("PlayerOne*"); - prepareEvent.setDecision(AccountDecision.deny("use premium entrypoint")); - } - return null; - }).when(eventManager).call(any()); - - PrepareDecision decision = pipeline.prepare(PrepareRequest.builder() - .stage(PrepareStage.PROFILE) - .connectionKey(connectionKey) - .identity(identity) - .build()) - .toCompletableFuture() - .join(); - - assertNotNull(decision); - assertTrue(decision.isDenied()); - assertEquals("use premium entrypoint", decision.getDenialMessage()); - assertEquals("PlayerOne*", decision.getEffectiveUsername()); - assertEquals(accountUniqueId, decision.getAccountUniqueId()); - assertNotNull(decision.getProvider()); - assertEquals("premium", decision.getProvider().getProviderId()); - verify(eventManager).call(any(AccountPrepareEvent.class)); - } - - @DisplayName("Handshake preparation reuses the decision that was already stored for the connection") - @Test - void handshakeStageReusesPreviousHandshakeDecision() { - TestPrepareStateStore prepareStateStore = new TestPrepareStateStore(); - PreparePipeline pipeline = pipeline(prepareStateStore); - ConnectionIdentity identity = identity("PlayerTwo"); - String connectionKey = "PlayerTwo|127.0.0.1|premium.example.com|25565"; - PrepareDecision previous = PrepareDecision.builder() - .status(PrepareDecision.Status.ALLOW) - .handshake(HandshakeDecision.allow()) - .effectiveUsername("PlayerTwo") - .build(); - prepareStateStore.put(connectionKey, previous); - - PrepareDecision decision = pipeline.prepare(PrepareRequest.builder() - .stage(PrepareStage.HANDSHAKE) - .connectionKey(connectionKey) - .identity(identity) - .build()) - .toCompletableFuture() - .join(); - - assertNotNull(decision); - assertEquals(PrepareDecision.Status.ALLOW, decision.getStatus()); - assertNotNull(decision.getHandshake()); - } - - @DisplayName("Handshake preparation exposes pending migration provider context before policy evaluation") - @Test - void handshakeStageUsesPendingMigrationProviderContext() { - ConnectionIdentity identity = identity("MigratingPlayer"); - TestPrepareStateStore prepareStateStore = new TestPrepareStateStore(); - PreparePipeline pipeline = pipeline(prepareStateStore); - String connectionKey = "MigratingPlayer|127.0.0.1|premium.example.com|25565"; - - PipelineState pendingMigrationState = PipelineState.initial(); - pendingMigrationState.setPipelineType(PipelineType.MIGRATION); - MigrationContext migrationContext = MigrationContext.builder() - .connectionUniqueId(UUID.randomUUID()) - .identity(new ConnectionIdentity(UUID.randomUUID(), "MigratingPlayer", "127.0.0.1")) - .targetProviderId("premium") - .build(); - migrationContext.setProvider(ProviderContext.of("premium", null, "MigratingPlayer", ProviderOrigin.MANUAL)); - pendingMigrationState.setScenario(migrationContext); - pendingMigrationState.putItem(new MigrationPendingState( - "premium", - 1234L, - MigrationInitiator.USER, - UUID.randomUUID() - ), 1_000L); - - when(handshakeStore.policies()).thenReturn(java.util.Set.of()); - when(pipelineStateStore.find(argThat((PipelineStateReference reference) -> connectionKey.equals(reference.getConnectionKey())))) - .thenReturn(Optional.of(pendingMigrationState)); - - PrepareDecision decision = pipeline.prepare(PrepareRequest.builder() - .stage(PrepareStage.HANDSHAKE) - .connectionKey(connectionKey) - .identity(identity) - .build()) - .toCompletableFuture() - .join(); - - assertNotNull(decision); - assertEquals(PrepareDecision.Status.ALLOW, decision.getStatus()); - assertNotNull(decision.getProvider()); - assertEquals("premium", decision.getProvider().getProviderId()); - } - - @DisplayName("Handshake preparation evaluates only the policy for the selected provider") - @Test - void handshakeStageSkipsScopedPoliciesForOtherProviders() { - ConnectionIdentity identity = identity("MigratingPlayer"); - TestPrepareStateStore prepareStateStore = new TestPrepareStateStore(); - PreparePipeline pipeline = pipeline(prepareStateStore); - String connectionKey = "MigratingPlayer|127.0.0.1|premium.example.com|25565"; - - PipelineState pendingMigrationState = PipelineState.initial(); - pendingMigrationState.setPipelineType(PipelineType.MIGRATION); - MigrationContext migrationContext = MigrationContext.builder() - .connectionUniqueId(UUID.randomUUID()) - .identity(new ConnectionIdentity(UUID.randomUUID(), "MigratingPlayer", "127.0.0.1")) - .targetProviderId("credential") - .build(); - migrationContext.setProvider(ProviderContext.of("credential", "offline-subject", "MigratingPlayer", ProviderOrigin.MANUAL)); - pendingMigrationState.setScenario(migrationContext); - pendingMigrationState.putItem(new MigrationPendingState( - "credential", - 1234L, - MigrationInitiator.USER, - UUID.randomUUID() - ), 1_000L); - - CountingScopedHandshakePolicy premiumPolicy = new CountingScopedHandshakePolicy("premium"); - when(handshakeStore.policies()).thenReturn(java.util.Set.of(premiumPolicy)); - when(pipelineStateStore.find(argThat((PipelineStateReference reference) -> connectionKey.equals(reference.getConnectionKey())))) - .thenReturn(Optional.of(pendingMigrationState)); - - PrepareDecision decision = pipeline.prepare(PrepareRequest.builder() - .stage(PrepareStage.HANDSHAKE) - .connectionKey(connectionKey) - .identity(identity) - .build()) - .toCompletableFuture() - .join(); - - assertNotNull(decision); - assertEquals(PrepareDecision.Status.ALLOW, decision.getStatus()); - assertEquals(0, premiumPolicy.invocations); - } - - @DisplayName("Handshake preparation skips provider-scoped handshake policies on blocked untrusted IPs") - @Test - void handshakeStageSkipsScopedPoliciesOnBlockedUntrustedIp() { - ConnectionIdentity identity = identity("MigratingPlayer"); - TestPrepareStateStore prepareStateStore = new TestPrepareStateStore(); - PreparePipeline pipeline = pipeline(prepareStateStore); - String connectionKey = "MigratingPlayer|127.0.0.1|premium.example.com|25565"; - - PipelineState pendingMigrationState = PipelineState.initial(); - pendingMigrationState.setPipelineType(PipelineType.MIGRATION); - MigrationContext migrationContext = MigrationContext.builder() - .connectionUniqueId(UUID.randomUUID()) - .identity(new ConnectionIdentity(UUID.randomUUID(), "MigratingPlayer", "127.0.0.1")) - .targetProviderId("premium") - .build(); - ProviderContext providerContext = ProviderContext.of("premium", null, "MigratingPlayer", ProviderOrigin.AUTO); - migrationContext.setProvider(providerContext); - pendingMigrationState.setScenario(migrationContext); - pendingMigrationState.putItem(new MigrationPendingState( - "premium", - 1234L, - MigrationInitiator.USER, - UUID.randomUUID() - ), 1_000L); - - CountingScopedHandshakePolicy premiumPolicy = new CountingScopedHandshakePolicy("premium"); - when(recognitionEligibilityService.evaluate(any())) - .thenReturn(blockedDecision()); - when(handshakeStore.policies()).thenReturn(java.util.Set.of(premiumPolicy)); - when(pipelineStateStore.find(argThat((PipelineStateReference reference) -> connectionKey.equals(reference.getConnectionKey())))) - .thenReturn(Optional.of(pendingMigrationState)); - - PrepareDecision decision = pipeline.prepare(PrepareRequest.builder() - .stage(PrepareStage.HANDSHAKE) - .connectionKey(connectionKey) - .identity(identity) - .build()) - .toCompletableFuture() - .join(); - - assertNotNull(decision); - assertEquals(PrepareDecision.Status.ALLOW, decision.getStatus()); - assertEquals(0, premiumPolicy.invocations); - } - - @DisplayName("Premium auto-recognition handshake path is skipped on blocked untrusted IPs") - @Test - void premiumAutoRecognitionHandshakePathIsSkippedOnBlockedUntrustedIp() { - ConnectionIdentity identity = identity("MigratingPlayer"); - TestPrepareStateStore prepareStateStore = new TestPrepareStateStore(); - PreparePipeline pipeline = pipeline(prepareStateStore); - String connectionKey = "MigratingPlayer|127.0.0.1|premium.example.com|25565"; - - PipelineState pendingMigrationState = PipelineState.initial(); - pendingMigrationState.setPipelineType(PipelineType.MIGRATION); - MigrationContext migrationContext = MigrationContext.builder() - .connectionUniqueId(UUID.randomUUID()) - .identity(new ConnectionIdentity(UUID.randomUUID(), "MigratingPlayer", "127.0.0.1")) - .targetProviderId("premium") - .build(); - ProviderContext providerContext = ProviderContext.of("premium", null, "MigratingPlayer", ProviderOrigin.AUTO); - migrationContext.setProvider(providerContext); - pendingMigrationState.setScenario(migrationContext); - pendingMigrationState.putItem(new MigrationPendingState( - "premium", - 1234L, - MigrationInitiator.USER, - UUID.randomUUID() - ), 1_000L); - - CountingScopedHandshakePolicy premiumPolicy = new CountingScopedHandshakePolicy("premium"); - when(recognitionEligibilityService.evaluate(any())) - .thenReturn(blockedDecision()); - when(handshakeStore.policies()).thenReturn(java.util.Set.of(premiumPolicy)); - when(pipelineStateStore.find(argThat((PipelineStateReference reference) -> connectionKey.equals(reference.getConnectionKey())))) - .thenReturn(Optional.of(pendingMigrationState)); - - PrepareDecision decision = pipeline.prepare(PrepareRequest.builder() - .stage(PrepareStage.HANDSHAKE) - .connectionKey(connectionKey) - .identity(identity) - .build()) - .toCompletableFuture() - .join(); - - assertNotNull(decision); - assertEquals(PrepareDecision.Status.ALLOW, decision.getStatus()); - assertEquals(0, premiumPolicy.invocations); - } - - @DisplayName("Provider-scoped handshake still runs when the provider override allows recognition") - @Test - void handshakeStageRunsScopedPolicyWhenOverrideAllowsRecognition() { - ConnectionIdentity identity = identity("MigratingPlayer"); - TestPrepareStateStore prepareStateStore = new TestPrepareStateStore(); - PreparePipeline pipeline = pipeline(prepareStateStore); - String connectionKey = "MigratingPlayer|127.0.0.1|premium.example.com|25565"; - - PipelineState pendingMigrationState = PipelineState.initial(); - pendingMigrationState.setPipelineType(PipelineType.MIGRATION); - MigrationContext migrationContext = MigrationContext.builder() - .connectionUniqueId(UUID.randomUUID()) - .identity(new ConnectionIdentity(UUID.randomUUID(), "MigratingPlayer", "127.0.0.1")) - .targetProviderId("premium") - .build(); - ProviderContext providerContext = ProviderContext.of("premium", null, "MigratingPlayer", ProviderOrigin.AUTO); - migrationContext.setProvider(providerContext); - pendingMigrationState.setScenario(migrationContext); - pendingMigrationState.putItem(new MigrationPendingState( - "premium", - 1234L, - MigrationInitiator.USER, - UUID.randomUUID() - ), 1_000L); - - CountingScopedHandshakePolicy premiumPolicy = new CountingScopedHandshakePolicy("premium"); - when(recognitionEligibilityService.evaluate(any())) - .thenReturn(allowedDecision()); - when(handshakeStore.policies()).thenReturn(java.util.Set.of(premiumPolicy)); - when(pipelineStateStore.find(argThat((PipelineStateReference reference) -> connectionKey.equals(reference.getConnectionKey())))) - .thenReturn(Optional.of(pendingMigrationState)); - - PrepareDecision decision = pipeline.prepare(PrepareRequest.builder() - .stage(PrepareStage.HANDSHAKE) - .connectionKey(connectionKey) - .identity(identity) - .build()) - .toCompletableFuture() - .join(); - - assertNotNull(decision); - assertEquals(PrepareDecision.Status.ALLOW, decision.getStatus()); - assertEquals(1, premiumPolicy.invocations); - } - - @DisplayName("Handshake still runs for explicitly selected providers on blocked untrusted IPs") - @Test - void handshakeStageRunsForExplicitProviderSelectionOnUntrustedIp() { - ConnectionIdentity identity = identity("MigratingPlayer"); - TestPrepareStateStore prepareStateStore = new TestPrepareStateStore(); - PreparePipeline pipeline = pipeline(prepareStateStore); - String connectionKey = "MigratingPlayer|127.0.0.1|premium.example.com|25565"; - - PipelineState pendingMigrationState = PipelineState.initial(); - pendingMigrationState.setPipelineType(PipelineType.MIGRATION); - MigrationContext migrationContext = MigrationContext.builder() - .connectionUniqueId(UUID.randomUUID()) - .identity(new ConnectionIdentity(UUID.randomUUID(), "MigratingPlayer", "127.0.0.1")) - .targetProviderId("premium") - .build(); - ProviderContext providerContext = ProviderContext.of("premium", null, "MigratingPlayer", ProviderOrigin.MANUAL); - migrationContext.setProvider(providerContext); - pendingMigrationState.setScenario(migrationContext); - pendingMigrationState.putItem(new MigrationPendingState( - "premium", - 1234L, - MigrationInitiator.USER, - UUID.randomUUID() - ), 1_000L); - - CountingScopedHandshakePolicy premiumPolicy = new CountingScopedHandshakePolicy("premium"); - when(recognitionEligibilityService.evaluate(any())) - .thenReturn(allowedDecision()); - when(handshakeStore.policies()).thenReturn(java.util.Set.of(premiumPolicy)); - when(pipelineStateStore.find(argThat((PipelineStateReference reference) -> connectionKey.equals(reference.getConnectionKey())))) - .thenReturn(Optional.of(pendingMigrationState)); - - PrepareDecision decision = pipeline.prepare(PrepareRequest.builder() - .stage(PrepareStage.HANDSHAKE) - .connectionKey(connectionKey) - .identity(identity) - .build()) - .toCompletableFuture() - .join(); - - assertNotNull(decision); - assertEquals(PrepareDecision.Status.ALLOW, decision.getStatus()); - assertEquals(1, premiumPolicy.invocations); - } - - @DisplayName("Non-provider-scoped handshake policies still run when provider-scoped recognition is blocked") - @Test - void handshakeStageKeepsNonProviderScopedPoliciesWhenScopedRecognitionBlocked() { - ConnectionIdentity identity = identity("MigratingPlayer"); - TestPrepareStateStore prepareStateStore = new TestPrepareStateStore(); - PreparePipeline pipeline = pipeline(prepareStateStore); - String connectionKey = "MigratingPlayer|127.0.0.1|premium.example.com|25565"; - - PipelineState pendingMigrationState = PipelineState.initial(); - pendingMigrationState.setPipelineType(PipelineType.MIGRATION); - MigrationContext migrationContext = MigrationContext.builder() - .connectionUniqueId(UUID.randomUUID()) - .identity(new ConnectionIdentity(UUID.randomUUID(), "MigratingPlayer", "127.0.0.1")) - .targetProviderId("premium") - .build(); - ProviderContext providerContext = ProviderContext.of("premium", null, "MigratingPlayer", ProviderOrigin.AUTO); - migrationContext.setProvider(providerContext); - pendingMigrationState.setScenario(migrationContext); - pendingMigrationState.putItem(new MigrationPendingState( - "premium", - 1234L, - MigrationInitiator.USER, - UUID.randomUUID() - ), 1_000L); - - CountingScopedHandshakePolicy premiumPolicy = new CountingScopedHandshakePolicy("premium"); - CountingHandshakePolicy globalPolicy = new CountingHandshakePolicy(); - when(recognitionEligibilityService.evaluate(any())) - .thenReturn(blockedDecision()); - when(handshakeStore.policies()).thenReturn(java.util.Set.of(premiumPolicy, globalPolicy)); - when(pipelineStateStore.find(argThat((PipelineStateReference reference) -> connectionKey.equals(reference.getConnectionKey())))) - .thenReturn(Optional.of(pendingMigrationState)); - - PrepareDecision decision = pipeline.prepare(PrepareRequest.builder() - .stage(PrepareStage.HANDSHAKE) - .connectionKey(connectionKey) - .identity(identity) - .build()) - .toCompletableFuture() - .join(); - - assertNotNull(decision); - assertEquals(PrepareDecision.Status.ALLOW, decision.getStatus()); - assertEquals(0, premiumPolicy.invocations); - assertEquals(1, globalPolicy.invocations); - } - - @DisplayName("Premium profile preparation reuses the UUID from an existing linked account") - @Test - void profileStageReusesExistingLinkedUuidForPremiumJoin() { - UUID accountUniqueId = UUID.randomUUID(); - ConnectionIdentity identity = identity("MigratedPlayer"); - TestPrepareStateStore prepareStateStore = new TestPrepareStateStore(); - PreparePipeline pipeline = pipeline(prepareStateStore); - String connectionKey = "MigratedPlayer|127.0.0.1|premium.example.com|25565"; - - when(handshakeStore.policies()).thenReturn(java.util.Set.of()); - when(providerOperations.resolveProfile(any())) - .thenReturn(ProfileResolution.builder() - .providerId("premium") - .providerSubject("premium-subject") - .build()); - when(providerOperations.resolveEntrypoint("premium.example.com", 25565)) - .thenReturn(ResolvedEntrypoint.builder() - .providerId("premium") - .host("premium.example.com") - .port(25565) - .build()); - when(registrationAccountService.reserve(any())).thenReturn(accountUniqueId); - when(providerLinkPersistenceService.findBySubject("premium", "premium-subject")) - .thenReturn(Optional.of(AccountProviderLink.builder() - .uniqueId(accountUniqueId) - .providerId("premium") - .providerSubject("premium-subject") - .primaryLink(true) - .build())); - when(accountPersistenceService.findByUniqueId(accountUniqueId)) - .thenReturn(Optional.of(Account.builder() - .uniqueId(accountUniqueId) - .username("MigratedPlayer") - .source(UsernameSource.PROVIDER) - .build())); - when(providerProfilePersistenceService.findBySubject("premium", "premium-subject")) - .thenReturn(Optional.of(AccountProviderProfile.builder() - .providerId("premium") - .providerSubject("premium-subject") - .providerUsername("MigratedPlayer") - .build())); - - PrepareDecision decision = pipeline.prepare(PrepareRequest.builder() - .stage(PrepareStage.PROFILE) - .connectionKey(connectionKey) - .identity(identity) - .build()) - .toCompletableFuture() - .join(); - - assertNotNull(decision); - assertEquals(PrepareDecision.Status.ALLOW, decision.getStatus()); - assertEquals(accountUniqueId, decision.getAccountUniqueId()); - assertEquals("MigratedPlayer", decision.getEffectiveUsername()); - assertNotNull(prepareStateStore.peek(accountUniqueId).orElse(null)); - } - - @DisplayName("Profile preparation reuses the UUID already prepared for the same connection") - @Test - void profileStageReusesPreparedUuidForSameConnectionKey() { - UUID preparedUniqueId = UUID.randomUUID(); - ConnectionIdentity firstIdentity = identity("whereareiam"); - ConnectionIdentity secondIdentity = identity("whereareiam"); - TestPrepareStateStore prepareStateStore = new TestPrepareStateStore(); - PreparePipeline pipeline = pipeline(prepareStateStore); - String connectionKey = "whereareiam|127.0.0.1|premium.example.com|25565"; - firstIdentity.setObservedUniqueId(UUID.randomUUID()); - secondIdentity.setObservedUniqueId(UUID.randomUUID()); - - when(handshakeStore.policies()).thenReturn(java.util.Set.of()); - when(providerOperations.resolveEntrypoint("premium.example.com", 25565)) - .thenReturn(ResolvedEntrypoint.builder() - .providerId("premium") - .host("premium.example.com") - .port(25565) - .build()); - when(providerOperations.resolveProfile(any())) - .thenReturn(ProfileResolution.builder() - .providerId("credential") - .providerSubject("offline-subject") - .build()) - .thenReturn(ProfileResolution.builder() - .providerId("premium") - .providerSubject("premium-subject") - .build()); - when(registrationAccountService.reserve(any())).thenReturn(preparedUniqueId); - when(providerLinkPersistenceService.findBySubject("credential", "offline-subject")) - .thenReturn(Optional.empty()); - when(providerLinkPersistenceService.findBySubject("premium", "premium-subject")) - .thenReturn(Optional.empty()); - when(accountPersistenceService.findByUniqueId(preparedUniqueId)) - .thenReturn(Optional.empty()); - when(providerProfilePersistenceService.findBySubject("credential", "offline-subject")) - .thenReturn(Optional.empty()); - when(providerProfilePersistenceService.findBySubject("premium", "premium-subject")) - .thenReturn(Optional.empty()); - - PrepareDecision first = pipeline.prepare(PrepareRequest.builder() - .stage(PrepareStage.PROFILE) - .connectionKey(connectionKey) - .identity(firstIdentity) - .build()) - .toCompletableFuture() - .join(); - PrepareDecision second = pipeline.prepare(PrepareRequest.builder() - .stage(PrepareStage.PROFILE) - .connectionKey(connectionKey) - .identity(secondIdentity) - .build()) - .toCompletableFuture() - .join(); - - assertNotNull(first); - assertNotNull(second); - assertEquals(preparedUniqueId, first.getAccountUniqueId()); - assertEquals(preparedUniqueId, second.getAccountUniqueId()); - verify(registrationAccountService).reserve(any()); - } - - @DisplayName("Pending migration state can supply the target account UUID during profile preparation") - @Test - void profileStageReusesPendingMigrationAccountForTargetProvider() { - UUID accountUniqueId = UUID.randomUUID(); - ConnectionIdentity identity = identity("MigratingPlayer"); - TestPrepareStateStore prepareStateStore = new TestPrepareStateStore(); - PreparePipeline pipeline = pipeline(prepareStateStore); - String connectionKey = "MigratingPlayer|127.0.0.1|premium.example.com|25565"; - identity.setObservedUniqueId(UUID.randomUUID()); - - PipelineState pendingMigrationState = PipelineState.initial(); - pendingMigrationState.setPipelineType(PipelineType.MIGRATION); - pendingMigrationState.setScenario(MigrationContext.builder() - .connectionUniqueId(UUID.randomUUID()) - .identity(new ConnectionIdentity(accountUniqueId, "MigratingPlayer", "127.0.0.1")) - .targetProviderId("premium") - .build()); - pendingMigrationState.putItem(new MigrationPendingState( - "premium", - 1234L, - MigrationInitiator.USER, - accountUniqueId - ), 1_000L); - - when(handshakeStore.policies()).thenReturn(java.util.Set.of()); - when(providerOperations.resolveProfile(any())) - .thenReturn(ProfileResolution.builder() - .providerId("premium") - .providerSubject("premium-subject") - .build()); - when(providerOperations.resolveEntrypoint("premium.example.com", 25565)) - .thenReturn(ResolvedEntrypoint.builder() - .providerId("premium") - .host("premium.example.com") - .port(25565) - .build()); - when(pipelineStateStore.find(org.mockito.ArgumentMatchers.any())).thenReturn(Optional.empty()); - when(pipelineStateStore.find(argThat((PipelineStateReference reference) -> connectionKey.equals(reference.getConnectionKey())))) - .thenReturn(Optional.of(pendingMigrationState)); - when(providerLinkPersistenceService.findBySubject("premium", "premium-subject")) - .thenReturn(Optional.empty()); - when(accountPersistenceService.findByUniqueId(accountUniqueId)) - .thenReturn(Optional.of(Account.builder() - .uniqueId(accountUniqueId) - .username("MigratingPlayer") - .source(UsernameSource.PROVIDER) - .build())); - when(providerProfilePersistenceService.findBySubject("premium", "premium-subject")) - .thenReturn(Optional.empty()); - - PrepareDecision decision = pipeline.prepare(PrepareRequest.builder() - .stage(PrepareStage.PROFILE) - .connectionKey(connectionKey) - .identity(identity) - .build()) - .toCompletableFuture() - .join(); - - assertNotNull(decision); - assertEquals(PrepareDecision.Status.ALLOW, decision.getStatus()); - assertEquals(accountUniqueId, decision.getAccountUniqueId()); - verify(registrationAccountService, never()).reserve(any()); - } - - @DisplayName("Profile preparation clears stale migration state when the observed provider no longer matches") - @Test - void profileStageClearsPendingMigrationWhenObservedProviderDiffers() { - UUID accountUniqueId = UUID.randomUUID(); - ConnectionIdentity identity = identity("MigratingPlayer"); - TestPrepareStateStore prepareStateStore = new TestPrepareStateStore(); - PreparePipeline pipeline = pipeline(prepareStateStore); - String connectionKey = "MigratingPlayer|127.0.0.1|premium.example.com|25565"; - identity.setObservedUniqueId(UUID.randomUUID()); - - PipelineState pendingMigrationState = PipelineState.initial(); - pendingMigrationState.setPipelineType(PipelineType.MIGRATION); - pendingMigrationState.setScenario(MigrationContext.builder() - .connectionUniqueId(UUID.randomUUID()) - .identity(new ConnectionIdentity(accountUniqueId, "MigratingPlayer", "127.0.0.1")) - .targetProviderId("premium") - .build()); - pendingMigrationState.putItem(new MigrationPendingState( - "premium", - 1234L, - MigrationInitiator.USER, - accountUniqueId - ), 1_000L); - - when(handshakeStore.policies()).thenReturn(java.util.Set.of()); - when(providerOperations.resolveProfile(any())) - .thenReturn(ProfileResolution.builder() - .providerId("credential") - .providerSubject("credential-subject") - .build()); - when(providerOperations.resolveEntrypoint("premium.example.com", 25565)) - .thenReturn(ResolvedEntrypoint.builder() - .providerId("premium") - .host("premium.example.com") - .port(25565) - .build()); - when(pipelineStateStore.find(org.mockito.ArgumentMatchers.any())).thenReturn(Optional.empty()); - when(pipelineStateStore.find(argThat((PipelineStateReference reference) -> connectionKey.equals(reference.getConnectionKey())))) - .thenReturn(Optional.of(pendingMigrationState)); - when(registrationAccountService.reserve(any())).thenReturn(accountUniqueId); - when(providerLinkPersistenceService.findBySubject("credential", "credential-subject")) - .thenReturn(Optional.empty()); - when(accountPersistenceService.findByUniqueId(accountUniqueId)) - .thenReturn(Optional.empty()); - when(providerProfilePersistenceService.findBySubject("credential", "credential-subject")) - .thenReturn(Optional.empty()); - - PrepareDecision decision = pipeline.prepare(PrepareRequest.builder() - .stage(PrepareStage.PROFILE) - .connectionKey(connectionKey) - .identity(identity) - .build()) - .toCompletableFuture() - .join(); - - assertNotNull(decision); - assertEquals(PrepareDecision.Status.ALLOW, decision.getStatus()); - assertEquals(accountUniqueId, decision.getAccountUniqueId()); - verify(pipelineStateStore).clear(argThat((PipelineStateReference reference) -> connectionKey.equals(reference.getConnectionKey()))); - } - - private @NotNull PreparePipeline pipeline(@NotNull PrepareStateStore prepareStateStore) { - ConnectionProviderContextResolver contextResolver = new ConnectionProviderContextResolver(providerOperations); - PreparePipelineRegistry registry = new PreparePipelineRegistry( - new ContextGroup(), - new HandshakeGroup(), - new ProfileGroup(), - new PolicyGroup(), - new FinalizeGroup(), - new RestorePrepareStatePhase(prepareStateStore), - new ResolveEntrypointPhase(providerOperations, contextResolver), - new ResolvePendingMigrationContextPhase(pipelineStateStore), - new EvaluateHandshakePhase(handshakeStore, recognitionEligibilityService), - new FinalizeHandshakePhase(eventManager, Messages::new), - new ResolveProfilePhase(providerOperations, contextResolver), - new ResolvePendingMigrationAccountPhase( - pipelineStateStore, - accountPersistenceService, - providerLinkPersistenceService, - providerProfilePersistenceService, - deliveryService - ), - new ResolvePreparedAccountPhase( - prepareStateStore, - accountPersistenceService, - providerLinkPersistenceService, - providerProfilePersistenceService - ), - new LoadPrepareAccountPhase( - registrationAccountService, - accountPersistenceService, - providerLinkPersistenceService, - providerProfilePersistenceService - ), - new ApplyProviderJoinRestrictionPhase( - providerJoinRestrictionService, - providerOperations, - Messages::new - ), - new ApplyPreparePolicyPhase(eventManager, Messages::new), - new StorePrepareDecisionPhase(prepareStateStore) - ); - return new PreparePipeline(registry, new PipelineExecutor()); - } - - private @NotNull ConnectionIdentity identity(@NotNull String username) { - ConnectionIdentity identity = new ConnectionIdentity(username, "127.0.0.1"); - identity.setOrigin(new ConnectionIdentity.Origin("premium.example.com", 25565)); - return identity; - } - - private RecognitionEligibilityDecision allowedDecision() { - return RecognitionEligibilityDecision.builder() - .allowed(true) - .reason("allowed") - .ruleId("test-allow") - .build(); - } - - private RecognitionEligibilityDecision blockedDecision() { - return RecognitionEligibilityDecision.builder() - .allowed(false) - .reason("blocked") - .ruleId("test-block") - .build(); - } - - private static final class TestPrepareStateStore implements PrepareStateStore { - private final Map byKey = new HashMap<>(); - private final Map byUniqueId = new HashMap<>(); - - @Override - public void put(@NotNull String connectionKey, @NotNull PrepareDecision decision) { - byKey.put(connectionKey, decision); - } - - @Override - public void put(@NotNull UUID uniqueId, String connectionKey, @NotNull PrepareDecision decision) { - byUniqueId.put(uniqueId, decision); - if (connectionKey != null && !connectionKey.isBlank()) - byKey.put(connectionKey, decision); - } - - @Override - public @NotNull Optional peek(@NotNull String connectionKey) { - return Optional.ofNullable(byKey.get(connectionKey)); - } - - @Override - public @NotNull Optional peek(@NotNull UUID uniqueId) { - return Optional.ofNullable(byUniqueId.get(uniqueId)); - } - - @Override - public boolean clear(@NotNull UUID uniqueId) { - return byUniqueId.remove(uniqueId) != null; - } - } - - private static final class CountingScopedHandshakePolicy implements ProviderScopedHandshakePolicy { - private final String providerId; - private int invocations; - - private CountingScopedHandshakePolicy(String providerId) { - this.providerId = providerId; - } - - @Override - public @NotNull String providerId() { - return providerId; - } - - @Override - public java.util.concurrent.CompletionStage evaluate( - HandshakeRequest request - ) { - invocations++; - return java.util.concurrent.CompletableFuture.completedFuture(HandshakeDecision.allow()); - } - } - - private static final class CountingHandshakePolicy implements HandshakePolicy { - private int invocations; - - @Override - public java.util.concurrent.CompletionStage evaluate(HandshakeRequest request) { - invocations++; - return java.util.concurrent.CompletableFuture.completedFuture(HandshakeDecision.allow()); - } - } -} diff --git a/identica-feature/build.gradle.kts b/identica-feature/build.gradle.kts new file mode 100644 index 000000000..6c8344122 --- /dev/null +++ b/identica-feature/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + id("base") +} diff --git a/identica-feature/feature-verification/verification-api/build.gradle.kts b/identica-feature/feature-verification/verification-api/build.gradle.kts new file mode 100644 index 000000000..f480e05f9 --- /dev/null +++ b/identica-feature/feature-verification/verification-api/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("api") +} + +group = "me.whereareiam.identica.feature" + +dependencies { + api(projects.identicaApi) +} + +toolkitPublish { + artifactId.set("verification") +} diff --git a/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/VerificationFeature.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/VerificationFeature.java new file mode 100644 index 000000000..cd9670979 --- /dev/null +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/VerificationFeature.java @@ -0,0 +1,11 @@ +package me.whereareiam.identica.feature.verification; + +import me.whereareiam.identica.type.provider.ProviderFeature; +import org.jetbrains.annotations.NotNull; + +/** + * Built-in verification feature identifier. + */ +public final class VerificationFeature { + public static final @NotNull ProviderFeature FEATURE = ProviderFeature.of("verification"); +} diff --git a/identica-api/src/main/java/me/whereareiam/identica/verification/VerificationInteraction.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/VerificationInteraction.java similarity index 90% rename from identica-api/src/main/java/me/whereareiam/identica/verification/VerificationInteraction.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/VerificationInteraction.java index 6017972c2..2915cb4e6 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/verification/VerificationInteraction.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/VerificationInteraction.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.verification; +package me.whereareiam.identica.feature.verification; import org.jetbrains.annotations.NotNull; diff --git a/identica-api/src/main/java/me/whereareiam/identica/verification/VerificationMethod.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/VerificationMethod.java similarity index 72% rename from identica-api/src/main/java/me/whereareiam/identica/verification/VerificationMethod.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/VerificationMethod.java index 03a8ff334..ba6dd36b9 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/verification/VerificationMethod.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/VerificationMethod.java @@ -1,8 +1,8 @@ -package me.whereareiam.identica.verification; +package me.whereareiam.identica.feature.verification; -import me.whereareiam.identica.model.verification.VerificationMethodDescriptor; -import me.whereareiam.identica.verification.process.VerificationChallengeProcess; -import me.whereareiam.identica.verification.process.VerificationEnrollmentProcess; +import me.whereareiam.identica.feature.verification.model.VerificationMethodDescriptor; +import me.whereareiam.identica.feature.verification.process.VerificationChallengeProcess; +import me.whereareiam.identica.feature.verification.process.VerificationEnrollmentProcess; import org.jetbrains.annotations.NotNull; /** diff --git a/identica-api/src/main/java/me/whereareiam/identica/verification/VerificationRegistry.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/VerificationRegistry.java similarity index 95% rename from identica-api/src/main/java/me/whereareiam/identica/verification/VerificationRegistry.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/VerificationRegistry.java index a39ed5fef..266f88c52 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/verification/VerificationRegistry.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/VerificationRegistry.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.verification; +package me.whereareiam.identica.feature.verification; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-api/src/main/java/me/whereareiam/identica/verification/VerificationService.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/VerificationService.java similarity index 80% rename from identica-api/src/main/java/me/whereareiam/identica/verification/VerificationService.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/VerificationService.java index 03c432b55..ff419ee33 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/verification/VerificationService.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/VerificationService.java @@ -1,14 +1,14 @@ -package me.whereareiam.identica.verification; +package me.whereareiam.identica.feature.verification; -import me.whereareiam.identica.model.verification.VerificationDisableResult; -import me.whereareiam.identica.model.verification.VerificationResolutionRequest; -import me.whereareiam.identica.model.verification.VerificationResolutionResult; -import me.whereareiam.identica.model.verification.VerificationResetResult; -import me.whereareiam.identica.model.verification.challenge.VerificationChallengeResult; -import me.whereareiam.identica.model.verification.enrollment.VerificationEnrollment; -import me.whereareiam.identica.model.verification.enrollment.VerificationEnrollmentResult; -import me.whereareiam.identica.model.verification.selection.VerificationSelection; -import me.whereareiam.identica.model.verification.selection.VerificationSelectionResult; +import me.whereareiam.identica.feature.verification.model.VerificationDisableResult; +import me.whereareiam.identica.feature.verification.model.VerificationResetResult; +import me.whereareiam.identica.feature.verification.model.challenge.VerificationChallengeResult; +import me.whereareiam.identica.feature.verification.model.enrollment.VerificationEnrollment; +import me.whereareiam.identica.feature.verification.model.enrollment.VerificationEnrollmentResult; +import me.whereareiam.identica.feature.verification.model.resolution.VerificationResolutionRequest; +import me.whereareiam.identica.feature.verification.model.resolution.VerificationResolutionResult; +import me.whereareiam.identica.feature.verification.model.selection.VerificationSelection; +import me.whereareiam.identica.feature.verification.model.selection.VerificationSelectionResult; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-api/src/main/java/me/whereareiam/identica/command/ProtectedActionCommand.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/command/ProtectedActionCommand.java similarity index 82% rename from identica-api/src/main/java/me/whereareiam/identica/command/ProtectedActionCommand.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/command/ProtectedActionCommand.java index c2af58350..c8945bd96 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/command/ProtectedActionCommand.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/command/ProtectedActionCommand.java @@ -1,13 +1,14 @@ -package me.whereareiam.identica.command; - +package me.whereareiam.identica.feature.verification.command; + +import me.whereareiam.identica.command.SessionBoundCommand; +import me.whereareiam.identica.feature.verification.VerificationService; +import me.whereareiam.identica.feature.verification.model.challenge.VerificationChallengeResult; +import me.whereareiam.identica.feature.verification.model.interaction.CodeVerificationInteraction; +import me.whereareiam.identica.feature.verification.model.resolution.VerificationResolutionRequest; +import me.whereareiam.identica.feature.verification.model.resolution.VerificationResolutionResult; +import me.whereareiam.identica.feature.verification.type.status.VerificationChallengeStatus; +import me.whereareiam.identica.feature.verification.type.status.VerificationResolutionStatus; import me.whereareiam.identica.model.Session; -import me.whereareiam.identica.model.verification.VerificationResolutionRequest; -import me.whereareiam.identica.model.verification.VerificationResolutionResult; -import me.whereareiam.identica.model.verification.challenge.VerificationChallengeResult; -import me.whereareiam.identica.type.verification.VerificationResolutionStatus; -import me.whereareiam.identica.model.verification.interaction.CodeVerificationInteraction; -import me.whereareiam.identica.type.verification.VerificationChallengeStatus; -import me.whereareiam.identica.verification.VerificationService; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/config/VerificationCommands.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/config/VerificationCommands.java new file mode 100644 index 000000000..c6b0777f9 --- /dev/null +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/config/VerificationCommands.java @@ -0,0 +1,21 @@ +package me.whereareiam.identica.feature.verification.config; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import me.whereareiam.configura.ConfigDocument; +import me.whereareiam.identica.model.CommandDefinition; +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.Map; + +/** + * Verification feature command definition document. + */ +@Getter +@Setter +@ToString +public class VerificationCommands extends ConfigDocument { + private @NotNull Map commands = new HashMap<>(); +} diff --git a/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/config/VerificationMessages.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/config/VerificationMessages.java new file mode 100644 index 000000000..1a02645ef --- /dev/null +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/config/VerificationMessages.java @@ -0,0 +1,152 @@ +package me.whereareiam.identica.feature.verification.config; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import me.whereareiam.configura.ConfigDocument; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +/** + * Verification feature message configuration document. + */ +@Getter +@Setter +@ToString +public class VerificationMessages extends ConfigDocument { + private @NotNull Methods methods; + private @NotNull Commands commands; + + @Getter + @Setter + @ToString + public static class Commands { + private @NotNull String playerOnly; + private @NotNull String notAllowed; + private @NotNull Status status; + private @NotNull Enroll enroll; + private @NotNull Confirm confirm; + private @NotNull Use use; + private @NotNull Disable disable; + private @NotNull Cancel cancel; + private @NotNull Admin admin; + + @Getter + @Setter + @ToString + public static class Status { + private @NotNull List body; + private @NotNull EntryFormat enrollmentEntry; + private @NotNull EntryFormat selectionEntry; + private @NotNull String emptyEnrollments; + private @NotNull String emptySelections; + } + + @Getter + @Setter + @ToString + public static class Enroll { + private @NotNull String unknownMethod; + private @NotNull String alreadyEnrolled; + } + + @Getter + @Setter + @ToString + public static class Confirm { + private @NotNull String noPending; + private @NotNull String invalidCode; + private @NotNull String protectedActionSelectionRequired; + private @NotNull String protectedActionSessionRequired; + private @NotNull String methodUnavailable; + private @NotNull String enabled; + private @NotNull String autoSelected; + private @NotNull RecoveryCodes recoveryCodes; + + @Getter + @Setter + @ToString + public static class RecoveryCodes { + private @NotNull Layout layout = Layout.TWO_COLUMN; + private @NotNull List body; + private @NotNull EntryFormat singleColumnEntry; + private @NotNull EntryFormat twoColumnEntry; + private @NotNull String empty; + + public enum Layout { + SINGLE_COLUMN, + TWO_COLUMN + } + } + } + + @Getter + @Setter + @ToString + public static class Use { + private @NotNull String providerNotFound; + private @NotNull String providerUnsupported; + private @NotNull String providerVerificationDisabled; + private @NotNull String methodNotEnrolled; + private @NotNull String methodDisabledForProvider; + private @NotNull String alreadySelected; + private @NotNull String updated; + } + + @Getter + @Setter + @ToString + public static class Disable { + private @NotNull String methodNotEnrolled; + private @NotNull String protectedPrompt; + private @NotNull String disabled; + } + + @Getter + @Setter + @ToString + public static class Cancel { + private @NotNull String noPending; + private @NotNull String cancelled; + private @NotNull String cancelledProtectedAction; + } + + @Getter + @Setter + @ToString + public static class Admin { + private @NotNull Reset reset; + + @Getter + @Setter + @ToString + public static class Reset { + private @NotNull String targetNotFound; + private @NotNull String completed; + } + } + + @Getter + @Setter + @ToString + public static class EntryFormat { + private @NotNull String format; + private @NotNull String emptyFormat; + } + } + + @Getter + @Setter + @ToString + public static class Methods { + private @NotNull Totp totp; + + @Getter + @Setter + @ToString + public static class Totp { + private @NotNull List pending; + } + } +} diff --git a/identica-api/src/main/java/me/whereareiam/identica/database/VerificationPersistenceService.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/database/VerificationPersistenceService.java similarity index 82% rename from identica-api/src/main/java/me/whereareiam/identica/database/VerificationPersistenceService.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/database/VerificationPersistenceService.java index 6c0a82bc6..cb14f6391 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/database/VerificationPersistenceService.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/database/VerificationPersistenceService.java @@ -1,8 +1,8 @@ -package me.whereareiam.identica.database; +package me.whereareiam.identica.feature.verification.database; -import me.whereareiam.identica.model.verification.enrollment.VerificationEnrollment; -import me.whereareiam.identica.model.verification.VerificationRecoveryCode; -import me.whereareiam.identica.model.verification.selection.VerificationSelection; +import me.whereareiam.identica.feature.verification.model.VerificationRecoveryCode; +import me.whereareiam.identica.feature.verification.model.enrollment.VerificationEnrollment; +import me.whereareiam.identica.feature.verification.model.selection.VerificationSelection; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-api/src/main/java/me/whereareiam/identica/event/verification/VerificationMethodDisabledEvent.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/event/VerificationMethodDisabledEvent.java similarity index 90% rename from identica-api/src/main/java/me/whereareiam/identica/event/verification/VerificationMethodDisabledEvent.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/event/VerificationMethodDisabledEvent.java index 80aabca4d..102e91a8f 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/event/verification/VerificationMethodDisabledEvent.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/event/VerificationMethodDisabledEvent.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.event.verification; +package me.whereareiam.identica.feature.verification.event; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/identica-api/src/main/java/me/whereareiam/identica/event/verification/VerificationMethodRegisteredEvent.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/event/VerificationMethodRegisteredEvent.java similarity index 76% rename from identica-api/src/main/java/me/whereareiam/identica/event/verification/VerificationMethodRegisteredEvent.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/event/VerificationMethodRegisteredEvent.java index 44ca6e346..6c67ab2dd 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/event/verification/VerificationMethodRegisteredEvent.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/event/VerificationMethodRegisteredEvent.java @@ -1,11 +1,11 @@ -package me.whereareiam.identica.event.verification; +package me.whereareiam.identica.feature.verification.event; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.ToString; import me.whereareiam.identica.event.base.Event; import me.whereareiam.identica.event.base.SynchronousEvent; -import me.whereareiam.identica.model.verification.VerificationMethodDescriptor; +import me.whereareiam.identica.feature.verification.model.VerificationMethodDescriptor; import org.jetbrains.annotations.NotNull; /** diff --git a/identica-api/src/main/java/me/whereareiam/identica/event/verification/VerificationMethodUnregisteredEvent.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/event/VerificationMethodUnregisteredEvent.java similarity index 76% rename from identica-api/src/main/java/me/whereareiam/identica/event/verification/VerificationMethodUnregisteredEvent.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/event/VerificationMethodUnregisteredEvent.java index 2947b5983..284a3a533 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/event/verification/VerificationMethodUnregisteredEvent.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/event/VerificationMethodUnregisteredEvent.java @@ -1,11 +1,11 @@ -package me.whereareiam.identica.event.verification; +package me.whereareiam.identica.feature.verification.event; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.ToString; import me.whereareiam.identica.event.base.Event; import me.whereareiam.identica.event.base.SynchronousEvent; -import me.whereareiam.identica.model.verification.VerificationMethodDescriptor; +import me.whereareiam.identica.feature.verification.model.VerificationMethodDescriptor; import org.jetbrains.annotations.NotNull; /** diff --git a/identica-api/src/main/java/me/whereareiam/identica/event/verification/VerificationResetEvent.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/event/VerificationResetEvent.java similarity index 92% rename from identica-api/src/main/java/me/whereareiam/identica/event/verification/VerificationResetEvent.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/event/VerificationResetEvent.java index 38be8ab00..ebc087296 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/event/verification/VerificationResetEvent.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/event/VerificationResetEvent.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.event.verification; +package me.whereareiam.identica.feature.verification.event; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/identica-api/src/main/java/me/whereareiam/identica/event/verification/VerificationSelectionEvent.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/event/VerificationSelectionEvent.java similarity index 93% rename from identica-api/src/main/java/me/whereareiam/identica/event/verification/VerificationSelectionEvent.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/event/VerificationSelectionEvent.java index 4787d2c47..4572c0784 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/event/verification/VerificationSelectionEvent.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/event/VerificationSelectionEvent.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.event.verification; +package me.whereareiam.identica.feature.verification.event; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/identica-api/src/main/java/me/whereareiam/identica/event/verification/challenge/VerificationChallengeEvent.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/event/challenge/VerificationChallengeEvent.java similarity index 77% rename from identica-api/src/main/java/me/whereareiam/identica/event/verification/challenge/VerificationChallengeEvent.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/event/challenge/VerificationChallengeEvent.java index 208ec33ac..07373d19d 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/event/verification/challenge/VerificationChallengeEvent.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/event/challenge/VerificationChallengeEvent.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.event.verification.challenge; +package me.whereareiam.identica.feature.verification.event.challenge; import lombok.AllArgsConstructor; import lombok.Getter; @@ -6,8 +6,8 @@ import lombok.ToString; import me.whereareiam.identica.event.base.Event; import me.whereareiam.identica.event.base.SynchronousEvent; -import me.whereareiam.identica.model.verification.challenge.VerificationChallengeResult; -import me.whereareiam.identica.verification.VerificationInteraction; +import me.whereareiam.identica.feature.verification.VerificationInteraction; +import me.whereareiam.identica.feature.verification.model.challenge.VerificationChallengeResult; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-api/src/main/java/me/whereareiam/identica/event/verification/challenge/VerificationChallengeFailedEvent.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/event/challenge/VerificationChallengeFailedEvent.java similarity index 82% rename from identica-api/src/main/java/me/whereareiam/identica/event/verification/challenge/VerificationChallengeFailedEvent.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/event/challenge/VerificationChallengeFailedEvent.java index d907ab155..2b4209189 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/event/verification/challenge/VerificationChallengeFailedEvent.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/event/challenge/VerificationChallengeFailedEvent.java @@ -1,11 +1,11 @@ -package me.whereareiam.identica.event.verification.challenge; +package me.whereareiam.identica.feature.verification.event.challenge; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.ToString; import me.whereareiam.identica.event.base.Event; import me.whereareiam.identica.event.base.SynchronousEvent; -import me.whereareiam.identica.type.verification.VerificationChallengeStatus; +import me.whereareiam.identica.feature.verification.type.status.VerificationChallengeStatus; import org.jetbrains.annotations.NotNull; import java.util.UUID; diff --git a/identica-api/src/main/java/me/whereareiam/identica/event/verification/challenge/VerificationChallengeStartedEvent.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/event/challenge/VerificationChallengeStartedEvent.java similarity index 89% rename from identica-api/src/main/java/me/whereareiam/identica/event/verification/challenge/VerificationChallengeStartedEvent.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/event/challenge/VerificationChallengeStartedEvent.java index 6081af01b..0003a3ee9 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/event/verification/challenge/VerificationChallengeStartedEvent.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/event/challenge/VerificationChallengeStartedEvent.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.event.verification.challenge; +package me.whereareiam.identica.feature.verification.event.challenge; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/identica-api/src/main/java/me/whereareiam/identica/event/verification/challenge/VerificationChallengeSucceededEvent.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/event/challenge/VerificationChallengeSucceededEvent.java similarity index 89% rename from identica-api/src/main/java/me/whereareiam/identica/event/verification/challenge/VerificationChallengeSucceededEvent.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/event/challenge/VerificationChallengeSucceededEvent.java index 17f7d059d..9de21229f 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/event/verification/challenge/VerificationChallengeSucceededEvent.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/event/challenge/VerificationChallengeSucceededEvent.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.event.verification.challenge; +package me.whereareiam.identica.feature.verification.event.challenge; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/identica-api/src/main/java/me/whereareiam/identica/event/verification/enroll/VerificationEnrollEvent.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/event/enroll/VerificationEnrollEvent.java similarity index 94% rename from identica-api/src/main/java/me/whereareiam/identica/event/verification/enroll/VerificationEnrollEvent.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/event/enroll/VerificationEnrollEvent.java index 1698211aa..5b0d52a48 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/event/verification/enroll/VerificationEnrollEvent.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/event/enroll/VerificationEnrollEvent.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.event.verification.enroll; +package me.whereareiam.identica.feature.verification.event.enroll; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/identica-api/src/main/java/me/whereareiam/identica/event/verification/enroll/VerificationEnrollmentConfirmedEvent.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/event/enroll/VerificationEnrollmentConfirmedEvent.java similarity index 91% rename from identica-api/src/main/java/me/whereareiam/identica/event/verification/enroll/VerificationEnrollmentConfirmedEvent.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/event/enroll/VerificationEnrollmentConfirmedEvent.java index 6a7bce59d..0da7bfbff 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/event/verification/enroll/VerificationEnrollmentConfirmedEvent.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/event/enroll/VerificationEnrollmentConfirmedEvent.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.event.verification.enroll; +package me.whereareiam.identica.feature.verification.event.enroll; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/pipeline/verification/VerificationDisablePendingState.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/VerificationDisablePendingState.java similarity index 87% rename from identica-api/src/main/java/me/whereareiam/identica/model/pipeline/verification/VerificationDisablePendingState.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/VerificationDisablePendingState.java index 591579061..9a726d4fb 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/pipeline/verification/VerificationDisablePendingState.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/VerificationDisablePendingState.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.model.pipeline.verification; +package me.whereareiam.identica.feature.verification.model; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/VerificationDisableResult.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/VerificationDisableResult.java new file mode 100644 index 000000000..662d1a3f9 --- /dev/null +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/VerificationDisableResult.java @@ -0,0 +1,18 @@ +package me.whereareiam.identica.feature.verification.model; + +import lombok.*; +import me.whereareiam.identica.feature.verification.type.status.VerificationDisableStatus; + +/** + * Result payload for verification method disable operations. + */ +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Builder(toBuilder = true) +public class VerificationDisableResult { + private VerificationDisableStatus status; + private String methodId; +} diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/verification/VerificationMethodDescriptor.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/VerificationMethodDescriptor.java similarity index 66% rename from identica-api/src/main/java/me/whereareiam/identica/model/verification/VerificationMethodDescriptor.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/VerificationMethodDescriptor.java index 070d62657..34013ddbe 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/verification/VerificationMethodDescriptor.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/VerificationMethodDescriptor.java @@ -1,12 +1,7 @@ -package me.whereareiam.identica.model.verification; +package me.whereareiam.identica.feature.verification.model; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; -import me.whereareiam.identica.type.verification.VerificationMethodCapability; +import lombok.*; +import me.whereareiam.identica.feature.verification.type.VerificationMethodCapability; import org.jetbrains.annotations.NotNull; import java.util.HashSet; diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/verification/VerificationRecoveryCode.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/VerificationRecoveryCode.java similarity index 57% rename from identica-api/src/main/java/me/whereareiam/identica/model/verification/VerificationRecoveryCode.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/VerificationRecoveryCode.java index 87a2bcc7f..af7f82181 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/verification/VerificationRecoveryCode.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/VerificationRecoveryCode.java @@ -1,11 +1,6 @@ -package me.whereareiam.identica.model.verification; +package me.whereareiam.identica.feature.verification.model; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; +import lombok.*; import java.util.UUID; diff --git a/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/VerificationResetResult.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/VerificationResetResult.java new file mode 100644 index 000000000..45a2a212b --- /dev/null +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/VerificationResetResult.java @@ -0,0 +1,18 @@ +package me.whereareiam.identica.feature.verification.model; + +import lombok.*; +import me.whereareiam.identica.feature.verification.type.status.VerificationResetStatus; + +/** + * Result payload for verification reset operations. + */ +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Builder(toBuilder = true) +public class VerificationResetResult { + private VerificationResetStatus status; + private String providerId; +} diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/verification/challenge/PendingVerificationChallenge.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/challenge/PendingVerificationChallenge.java similarity index 73% rename from identica-api/src/main/java/me/whereareiam/identica/model/verification/challenge/PendingVerificationChallenge.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/challenge/PendingVerificationChallenge.java index bad3f9911..fa119da1c 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/verification/challenge/PendingVerificationChallenge.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/challenge/PendingVerificationChallenge.java @@ -1,11 +1,6 @@ -package me.whereareiam.identica.model.verification.challenge; +package me.whereareiam.identica.feature.verification.model.challenge; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; +import lombok.*; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/verification/challenge/VerificationChallengeContext.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/challenge/VerificationChallengeContext.java similarity index 66% rename from identica-api/src/main/java/me/whereareiam/identica/model/verification/challenge/VerificationChallengeContext.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/challenge/VerificationChallengeContext.java index 4ba94cc2c..f39cb17ab 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/verification/challenge/VerificationChallengeContext.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/challenge/VerificationChallengeContext.java @@ -1,11 +1,7 @@ -package me.whereareiam.identica.model.verification.challenge; +package me.whereareiam.identica.feature.verification.model.challenge; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; +import lombok.*; +import me.whereareiam.identica.feature.verification.state.VerificationChallengeState; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -29,7 +25,7 @@ public class VerificationChallengeContext private @NotNull UUID subjectUniqueId; private @NotNull String methodId; @Builder.Default - private @NotNull List enrollments = new ArrayList<>(); + private @NotNull List enrollments = new ArrayList<>(); private @Nullable String providerId; private @Nullable String purpose; private boolean required; diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/verification/challenge/VerificationChallengeResult.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/challenge/VerificationChallengeResult.java similarity index 86% rename from identica-api/src/main/java/me/whereareiam/identica/model/verification/challenge/VerificationChallengeResult.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/challenge/VerificationChallengeResult.java index 2e456dff8..3bb7c6657 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/verification/challenge/VerificationChallengeResult.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/challenge/VerificationChallengeResult.java @@ -1,13 +1,9 @@ -package me.whereareiam.identica.model.verification.challenge; +package me.whereareiam.identica.feature.verification.model.challenge; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; -import me.whereareiam.identica.model.verification.process.VerificationProcessDisplay; -import me.whereareiam.identica.type.verification.VerificationChallengeStatus; +import lombok.*; +import me.whereareiam.identica.feature.verification.model.process.VerificationProcessDisplay; +import me.whereareiam.identica.feature.verification.state.VerificationChallengeState; +import me.whereareiam.identica.feature.verification.type.status.VerificationChallengeStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/config/VerificationProviders.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/config/VerificationProviders.java new file mode 100644 index 000000000..41dd5f092 --- /dev/null +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/config/VerificationProviders.java @@ -0,0 +1,51 @@ +package me.whereareiam.identica.feature.verification.model.config; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import me.whereareiam.configura.annotation.merge.Merge; +import me.whereareiam.configura.feature.extension.api.annotation.ExtendableDocument; +import me.whereareiam.configura.merge.strategy.DeclaredObjectDefaults; +import me.whereareiam.identica.feature.verification.type.UnavailableSelectionPolicy; +import me.whereareiam.identica.model.config.provider.Providers; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** + * Verification feature provider-specific configuration extension. + */ +@Getter +@Setter +@ToString +public class VerificationProviders extends Providers.ProviderEntry.Features { + @Merge(DeclaredObjectDefaults.class) + @ExtendableDocument + private @Nullable Verification verification; + + @Getter + @Setter + @ToString + @ExtendableDocument + public static class Verification { + private boolean enabled; + private boolean required; + private @Nullable UnavailableSelectionPolicy unavailableSelectionPolicy; + @Merge + private @NotNull List methods = new ArrayList<>(); + + @Getter + @Setter + @ToString + @ExtendableDocument + public static class MethodEntry { + private @NotNull String id = ""; + private boolean enabled; + private int priority; + private @Nullable Boolean required; + private @Nullable UnavailableSelectionPolicy unavailableSelectionPolicy; + } + } +} diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/config/Verification.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/config/VerificationSettings.java similarity index 92% rename from identica-api/src/main/java/me/whereareiam/identica/model/config/Verification.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/config/VerificationSettings.java index aebca1eeb..65d676461 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/config/Verification.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/config/VerificationSettings.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.model.config; +package me.whereareiam.identica.feature.verification.model.config; import lombok.Getter; import lombok.Setter; @@ -11,7 +11,7 @@ @Getter @Setter @ToString -public class Verification extends ConfigDocument { +public class VerificationSettings extends ConfigDocument { private @NotNull Duration challengeTtl; private @NotNull Duration enrollmentTtl; private boolean autoSelectCurrentProvider; diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/verification/enrollment/PendingVerificationEnrollment.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/enrollment/PendingVerificationEnrollment.java similarity index 73% rename from identica-api/src/main/java/me/whereareiam/identica/model/verification/enrollment/PendingVerificationEnrollment.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/enrollment/PendingVerificationEnrollment.java index 4c15f16a6..9406e35db 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/verification/enrollment/PendingVerificationEnrollment.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/enrollment/PendingVerificationEnrollment.java @@ -1,11 +1,6 @@ -package me.whereareiam.identica.model.verification.enrollment; +package me.whereareiam.identica.feature.verification.model.enrollment; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; +import lombok.*; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/verification/enrollment/VerificationEnrollment.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/enrollment/VerificationEnrollment.java similarity index 60% rename from identica-api/src/main/java/me/whereareiam/identica/model/verification/enrollment/VerificationEnrollment.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/enrollment/VerificationEnrollment.java index b3087b8ca..ad9c8201c 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/verification/enrollment/VerificationEnrollment.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/enrollment/VerificationEnrollment.java @@ -1,11 +1,6 @@ -package me.whereareiam.identica.model.verification.enrollment; +package me.whereareiam.identica.feature.verification.model.enrollment; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; +import lombok.*; import java.util.UUID; diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/verification/enrollment/VerificationEnrollmentContext.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/enrollment/VerificationEnrollmentContext.java similarity index 73% rename from identica-api/src/main/java/me/whereareiam/identica/model/verification/enrollment/VerificationEnrollmentContext.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/enrollment/VerificationEnrollmentContext.java index 1638f8ac3..a82af61c3 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/verification/enrollment/VerificationEnrollmentContext.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/enrollment/VerificationEnrollmentContext.java @@ -1,11 +1,7 @@ -package me.whereareiam.identica.model.verification.enrollment; +package me.whereareiam.identica.feature.verification.model.enrollment; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; +import lombok.*; +import me.whereareiam.identica.feature.verification.state.VerificationEnrollmentState; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/verification/enrollment/VerificationEnrollmentResult.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/enrollment/VerificationEnrollmentResult.java similarity index 85% rename from identica-api/src/main/java/me/whereareiam/identica/model/verification/enrollment/VerificationEnrollmentResult.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/enrollment/VerificationEnrollmentResult.java index ba60b3a2d..a6fb097a6 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/verification/enrollment/VerificationEnrollmentResult.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/enrollment/VerificationEnrollmentResult.java @@ -1,13 +1,9 @@ -package me.whereareiam.identica.model.verification.enrollment; +package me.whereareiam.identica.feature.verification.model.enrollment; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; -import me.whereareiam.identica.model.verification.process.VerificationProcessDisplay; -import me.whereareiam.identica.type.verification.VerificationEnrollmentStatus; +import lombok.*; +import me.whereareiam.identica.feature.verification.model.process.VerificationProcessDisplay; +import me.whereareiam.identica.feature.verification.state.VerificationEnrollmentState; +import me.whereareiam.identica.feature.verification.type.status.VerificationEnrollmentStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/verification/interaction/CodeVerificationInteraction.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/interaction/CodeVerificationInteraction.java similarity index 60% rename from identica-api/src/main/java/me/whereareiam/identica/model/verification/interaction/CodeVerificationInteraction.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/interaction/CodeVerificationInteraction.java index a6ca7fe50..a711d18b9 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/verification/interaction/CodeVerificationInteraction.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/interaction/CodeVerificationInteraction.java @@ -1,12 +1,7 @@ -package me.whereareiam.identica.model.verification.interaction; +package me.whereareiam.identica.feature.verification.model.interaction; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; -import me.whereareiam.identica.verification.VerificationInteraction; +import lombok.*; +import me.whereareiam.identica.feature.verification.VerificationInteraction; import org.jetbrains.annotations.NotNull; import java.util.UUID; diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/verification/interaction/RecoveryCodeVerificationInteraction.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/interaction/RecoveryCodeVerificationInteraction.java similarity index 61% rename from identica-api/src/main/java/me/whereareiam/identica/model/verification/interaction/RecoveryCodeVerificationInteraction.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/interaction/RecoveryCodeVerificationInteraction.java index 26c797eec..70699dc3b 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/verification/interaction/RecoveryCodeVerificationInteraction.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/interaction/RecoveryCodeVerificationInteraction.java @@ -1,12 +1,7 @@ -package me.whereareiam.identica.model.verification.interaction; +package me.whereareiam.identica.feature.verification.model.interaction; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; -import me.whereareiam.identica.verification.VerificationInteraction; +import lombok.*; +import me.whereareiam.identica.feature.verification.VerificationInteraction; import org.jetbrains.annotations.NotNull; import java.util.UUID; diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/verification/interaction/SavedVerificationInteraction.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/interaction/SavedVerificationInteraction.java similarity index 60% rename from identica-api/src/main/java/me/whereareiam/identica/model/verification/interaction/SavedVerificationInteraction.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/interaction/SavedVerificationInteraction.java index 4a8ab32f1..e9e54996d 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/verification/interaction/SavedVerificationInteraction.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/interaction/SavedVerificationInteraction.java @@ -1,12 +1,7 @@ -package me.whereareiam.identica.model.verification.interaction; +package me.whereareiam.identica.feature.verification.model.interaction; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; -import me.whereareiam.identica.verification.VerificationInteraction; +import lombok.*; +import me.whereareiam.identica.feature.verification.VerificationInteraction; import org.jetbrains.annotations.NotNull; import java.util.UUID; diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/verification/process/VerificationProcessContext.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/process/VerificationProcessContext.java similarity index 71% rename from identica-api/src/main/java/me/whereareiam/identica/model/verification/process/VerificationProcessContext.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/process/VerificationProcessContext.java index 411e3c58b..bae11f58a 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/verification/process/VerificationProcessContext.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/process/VerificationProcessContext.java @@ -1,12 +1,8 @@ -package me.whereareiam.identica.model.verification.process; +package me.whereareiam.identica.feature.verification.model.process; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; -import me.whereareiam.identica.model.verification.enrollment.VerificationEnrollment; +import lombok.*; +import me.whereareiam.identica.feature.verification.model.enrollment.VerificationEnrollment; +import me.whereareiam.identica.feature.verification.state.VerificationProcessState; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/verification/process/VerificationProcessDisplay.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/process/VerificationProcessDisplay.java similarity index 70% rename from identica-api/src/main/java/me/whereareiam/identica/model/verification/process/VerificationProcessDisplay.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/process/VerificationProcessDisplay.java index 12f4fbdfe..5c0a06c96 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/verification/process/VerificationProcessDisplay.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/process/VerificationProcessDisplay.java @@ -1,11 +1,6 @@ -package me.whereareiam.identica.model.verification.process; +package me.whereareiam.identica.feature.verification.model.process; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; +import lombok.*; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/verification/process/VerificationProcessResult.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/process/VerificationProcessResult.java similarity index 88% rename from identica-api/src/main/java/me/whereareiam/identica/model/verification/process/VerificationProcessResult.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/process/VerificationProcessResult.java index 31149378d..c15d4dc52 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/verification/process/VerificationProcessResult.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/process/VerificationProcessResult.java @@ -1,12 +1,8 @@ -package me.whereareiam.identica.model.verification.process; +package me.whereareiam.identica.feature.verification.model.process; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; -import me.whereareiam.identica.type.verification.VerificationProcessStatus; +import lombok.*; +import me.whereareiam.identica.feature.verification.state.VerificationProcessState; +import me.whereareiam.identica.feature.verification.type.process.VerificationProcessStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/verification/process/VerificationProcessTransition.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/process/VerificationProcessTransition.java similarity index 89% rename from identica-api/src/main/java/me/whereareiam/identica/model/verification/process/VerificationProcessTransition.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/process/VerificationProcessTransition.java index 63ea21099..791bc1fcf 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/verification/process/VerificationProcessTransition.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/process/VerificationProcessTransition.java @@ -1,11 +1,7 @@ -package me.whereareiam.identica.model.verification.process; +package me.whereareiam.identica.feature.verification.model.process; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; +import lombok.*; +import me.whereareiam.identica.feature.verification.type.process.VerificationProcessTransitionType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/verification/VerificationResolutionRequest.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/resolution/VerificationResolutionRequest.java similarity index 67% rename from identica-api/src/main/java/me/whereareiam/identica/model/verification/VerificationResolutionRequest.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/resolution/VerificationResolutionRequest.java index e4bb0a163..58d2ce28c 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/verification/VerificationResolutionRequest.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/resolution/VerificationResolutionRequest.java @@ -1,11 +1,6 @@ -package me.whereareiam.identica.model.verification; +package me.whereareiam.identica.feature.verification.model.resolution; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; +import lombok.*; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/verification/VerificationResolutionResult.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/resolution/VerificationResolutionResult.java similarity index 80% rename from identica-api/src/main/java/me/whereareiam/identica/model/verification/VerificationResolutionResult.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/resolution/VerificationResolutionResult.java index 83947776d..48a79e596 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/verification/VerificationResolutionResult.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/resolution/VerificationResolutionResult.java @@ -1,12 +1,7 @@ -package me.whereareiam.identica.model.verification; +package me.whereareiam.identica.feature.verification.model.resolution; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; -import me.whereareiam.identica.type.verification.VerificationResolutionStatus; +import lombok.*; +import me.whereareiam.identica.feature.verification.type.status.VerificationResolutionStatus; import org.jetbrains.annotations.Nullable; /** diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/verification/selection/VerificationSelection.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/selection/VerificationSelection.java similarity index 54% rename from identica-api/src/main/java/me/whereareiam/identica/model/verification/selection/VerificationSelection.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/selection/VerificationSelection.java index d8e86931c..e112805c4 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/verification/selection/VerificationSelection.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/selection/VerificationSelection.java @@ -1,11 +1,6 @@ -package me.whereareiam.identica.model.verification.selection; +package me.whereareiam.identica.feature.verification.model.selection; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; +import lombok.*; import java.util.UUID; diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/verification/selection/VerificationSelectionResult.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/selection/VerificationSelectionResult.java similarity index 72% rename from identica-api/src/main/java/me/whereareiam/identica/model/verification/selection/VerificationSelectionResult.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/selection/VerificationSelectionResult.java index e9e7b2ae2..4b87edcba 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/verification/selection/VerificationSelectionResult.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/model/selection/VerificationSelectionResult.java @@ -1,12 +1,7 @@ -package me.whereareiam.identica.model.verification.selection; +package me.whereareiam.identica.feature.verification.model.selection; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; -import me.whereareiam.identica.type.verification.status.VerificationSelectionStatus; +import lombok.*; +import me.whereareiam.identica.feature.verification.type.status.VerificationSelectionStatus; import org.jetbrains.annotations.Nullable; /** diff --git a/identica-api/src/main/java/me/whereareiam/identica/verification/process/VerificationChallengeProcess.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/process/VerificationChallengeProcess.java similarity index 69% rename from identica-api/src/main/java/me/whereareiam/identica/verification/process/VerificationChallengeProcess.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/process/VerificationChallengeProcess.java index 7946d1790..b1541ea57 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/verification/process/VerificationChallengeProcess.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/process/VerificationChallengeProcess.java @@ -1,9 +1,9 @@ -package me.whereareiam.identica.verification.process; +package me.whereareiam.identica.feature.verification.process; -import me.whereareiam.identica.model.verification.challenge.VerificationChallengeContext; -import me.whereareiam.identica.model.verification.challenge.VerificationChallengeResult; -import me.whereareiam.identica.model.verification.challenge.VerificationChallengeState; -import me.whereareiam.identica.verification.VerificationInteraction; +import me.whereareiam.identica.feature.verification.VerificationInteraction; +import me.whereareiam.identica.feature.verification.model.challenge.VerificationChallengeContext; +import me.whereareiam.identica.feature.verification.model.challenge.VerificationChallengeResult; +import me.whereareiam.identica.feature.verification.state.VerificationChallengeState; import org.jetbrains.annotations.NotNull; /** diff --git a/identica-api/src/main/java/me/whereareiam/identica/verification/process/VerificationEnrollmentProcess.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/process/VerificationEnrollmentProcess.java similarity index 68% rename from identica-api/src/main/java/me/whereareiam/identica/verification/process/VerificationEnrollmentProcess.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/process/VerificationEnrollmentProcess.java index af33e7f1c..fc06c53c5 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/verification/process/VerificationEnrollmentProcess.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/process/VerificationEnrollmentProcess.java @@ -1,9 +1,9 @@ -package me.whereareiam.identica.verification.process; +package me.whereareiam.identica.feature.verification.process; -import me.whereareiam.identica.model.verification.enrollment.VerificationEnrollmentContext; -import me.whereareiam.identica.model.verification.enrollment.VerificationEnrollmentResult; -import me.whereareiam.identica.model.verification.enrollment.VerificationEnrollmentState; -import me.whereareiam.identica.verification.VerificationInteraction; +import me.whereareiam.identica.feature.verification.VerificationInteraction; +import me.whereareiam.identica.feature.verification.model.enrollment.VerificationEnrollmentContext; +import me.whereareiam.identica.feature.verification.model.enrollment.VerificationEnrollmentResult; +import me.whereareiam.identica.feature.verification.state.VerificationEnrollmentState; import org.jetbrains.annotations.NotNull; /** diff --git a/identica-api/src/main/java/me/whereareiam/identica/verification/process/VerificationProcessStep.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/process/VerificationProcessStep.java similarity index 75% rename from identica-api/src/main/java/me/whereareiam/identica/verification/process/VerificationProcessStep.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/process/VerificationProcessStep.java index e7fa41db1..9dc9b591f 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/verification/process/VerificationProcessStep.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/process/VerificationProcessStep.java @@ -1,9 +1,9 @@ -package me.whereareiam.identica.verification.process; +package me.whereareiam.identica.feature.verification.process; -import me.whereareiam.identica.model.verification.process.VerificationProcessContext; -import me.whereareiam.identica.model.verification.process.VerificationProcessResult; -import me.whereareiam.identica.model.verification.process.VerificationProcessState; -import me.whereareiam.identica.verification.VerificationInteraction; +import me.whereareiam.identica.feature.verification.VerificationInteraction; +import me.whereareiam.identica.feature.verification.model.process.VerificationProcessContext; +import me.whereareiam.identica.feature.verification.model.process.VerificationProcessResult; +import me.whereareiam.identica.feature.verification.state.VerificationProcessState; import org.jetbrains.annotations.NotNull; /** diff --git a/identica-api/src/main/java/me/whereareiam/identica/verification/process/VerificationStepCursor.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/process/VerificationStepCursor.java similarity index 89% rename from identica-api/src/main/java/me/whereareiam/identica/verification/process/VerificationStepCursor.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/process/VerificationStepCursor.java index b795be253..b32eb4510 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/verification/process/VerificationStepCursor.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/process/VerificationStepCursor.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.verification.process; +package me.whereareiam.identica.feature.verification.process; import org.jetbrains.annotations.NotNull; diff --git a/identica-api/src/main/java/me/whereareiam/identica/verification/process/base/VerificationProcessOrchestrator.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/process/base/VerificationProcessOrchestrator.java similarity index 87% rename from identica-api/src/main/java/me/whereareiam/identica/verification/process/base/VerificationProcessOrchestrator.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/process/base/VerificationProcessOrchestrator.java index a14d7a5e5..73cd8def2 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/verification/process/base/VerificationProcessOrchestrator.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/process/base/VerificationProcessOrchestrator.java @@ -1,15 +1,15 @@ -package me.whereareiam.identica.verification.process.base; +package me.whereareiam.identica.feature.verification.process.base; import lombok.Getter; import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.model.verification.process.VerificationProcessContext; -import me.whereareiam.identica.model.verification.process.VerificationProcessResult; -import me.whereareiam.identica.model.verification.process.VerificationProcessState; -import me.whereareiam.identica.model.verification.process.VerificationProcessTransition; -import me.whereareiam.identica.model.verification.process.VerificationProcessTransitionType; -import me.whereareiam.identica.verification.VerificationInteraction; -import me.whereareiam.identica.verification.process.VerificationStepCursor; -import me.whereareiam.identica.verification.process.VerificationProcessStep; +import me.whereareiam.identica.feature.verification.VerificationInteraction; +import me.whereareiam.identica.feature.verification.model.process.VerificationProcessContext; +import me.whereareiam.identica.feature.verification.model.process.VerificationProcessResult; +import me.whereareiam.identica.feature.verification.model.process.VerificationProcessTransition; +import me.whereareiam.identica.feature.verification.process.VerificationProcessStep; +import me.whereareiam.identica.feature.verification.process.VerificationStepCursor; +import me.whereareiam.identica.feature.verification.state.VerificationProcessState; +import me.whereareiam.identica.feature.verification.type.process.VerificationProcessTransitionType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/state/VerificationChallengeState.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/state/VerificationChallengeState.java new file mode 100644 index 000000000..ebee51193 --- /dev/null +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/state/VerificationChallengeState.java @@ -0,0 +1,7 @@ +package me.whereareiam.identica.feature.verification.state; + +/** + * Marker for method-owned challenge state. + */ +public interface VerificationChallengeState extends VerificationProcessState { +} diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/verification/enrollment/VerificationEnrollmentState.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/state/VerificationEnrollmentState.java similarity index 60% rename from identica-api/src/main/java/me/whereareiam/identica/model/verification/enrollment/VerificationEnrollmentState.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/state/VerificationEnrollmentState.java index 134dc7f0f..43ddd296f 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/verification/enrollment/VerificationEnrollmentState.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/state/VerificationEnrollmentState.java @@ -1,6 +1,4 @@ -package me.whereareiam.identica.model.verification.enrollment; - -import me.whereareiam.identica.model.verification.process.VerificationProcessState; +package me.whereareiam.identica.feature.verification.state; /** * Marker for method-owned enrollment state. diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/verification/process/VerificationProcessState.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/state/VerificationProcessState.java similarity index 76% rename from identica-api/src/main/java/me/whereareiam/identica/model/verification/process/VerificationProcessState.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/state/VerificationProcessState.java index 20ff401ef..a9d6e7dd4 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/verification/process/VerificationProcessState.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/state/VerificationProcessState.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.model.verification.process; +package me.whereareiam.identica.feature.verification.state; import me.whereareiam.identica.pipeline.state.PipelineStateItem; diff --git a/identica-api/src/main/java/me/whereareiam/identica/type/verification/UnavailableSelectionPolicy.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/type/UnavailableSelectionPolicy.java similarity index 55% rename from identica-api/src/main/java/me/whereareiam/identica/type/verification/UnavailableSelectionPolicy.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/type/UnavailableSelectionPolicy.java index a72f4ece6..ebff39f61 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/type/verification/UnavailableSelectionPolicy.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/type/UnavailableSelectionPolicy.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.type.verification; +package me.whereareiam.identica.feature.verification.type; public enum UnavailableSelectionPolicy { CLEAR_SELECTION, diff --git a/identica-api/src/main/java/me/whereareiam/identica/type/verification/VerificationMethodCapability.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/type/VerificationMethodCapability.java similarity index 75% rename from identica-api/src/main/java/me/whereareiam/identica/type/verification/VerificationMethodCapability.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/type/VerificationMethodCapability.java index 078262bd7..950fe48dc 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/type/verification/VerificationMethodCapability.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/type/VerificationMethodCapability.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.type.verification; +package me.whereareiam.identica.feature.verification.type; /** * Capabilities advertised by verification methods. diff --git a/identica-api/src/main/java/me/whereareiam/identica/type/verification/VerificationProcessStatus.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/type/process/VerificationProcessStatus.java similarity index 74% rename from identica-api/src/main/java/me/whereareiam/identica/type/verification/VerificationProcessStatus.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/type/process/VerificationProcessStatus.java index 7550a6b3e..238484f1b 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/type/verification/VerificationProcessStatus.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/type/process/VerificationProcessStatus.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.type.verification; +package me.whereareiam.identica.feature.verification.type.process; /** * Generic status returned by a verification process step. diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/verification/process/VerificationProcessTransitionType.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/type/process/VerificationProcessTransitionType.java similarity index 86% rename from identica-api/src/main/java/me/whereareiam/identica/model/verification/process/VerificationProcessTransitionType.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/type/process/VerificationProcessTransitionType.java index 5e71b7c14..f2cf75e07 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/model/verification/process/VerificationProcessTransitionType.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/type/process/VerificationProcessTransitionType.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.model.verification.process; +package me.whereareiam.identica.feature.verification.type.process; /** * Transition modes returned by verification steps. diff --git a/identica-api/src/main/java/me/whereareiam/identica/type/verification/VerificationChallengeStatus.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/type/status/VerificationChallengeStatus.java similarity index 80% rename from identica-api/src/main/java/me/whereareiam/identica/type/verification/VerificationChallengeStatus.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/type/status/VerificationChallengeStatus.java index c34fb0e82..19124cf16 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/type/verification/VerificationChallengeStatus.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/type/status/VerificationChallengeStatus.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.type.verification; +package me.whereareiam.identica.feature.verification.type.status; /** * Status for an authentication or protected-action verification challenge. diff --git a/identica-api/src/main/java/me/whereareiam/identica/type/verification/status/VerificationDisableStatus.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/type/status/VerificationDisableStatus.java similarity index 81% rename from identica-api/src/main/java/me/whereareiam/identica/type/verification/status/VerificationDisableStatus.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/type/status/VerificationDisableStatus.java index fc62ee639..51692aba9 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/type/verification/status/VerificationDisableStatus.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/type/status/VerificationDisableStatus.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.type.verification.status; +package me.whereareiam.identica.feature.verification.type.status; /** * Outcome states for verification method disable operations. diff --git a/identica-api/src/main/java/me/whereareiam/identica/type/verification/VerificationEnrollmentStatus.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/type/status/VerificationEnrollmentStatus.java similarity index 78% rename from identica-api/src/main/java/me/whereareiam/identica/type/verification/VerificationEnrollmentStatus.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/type/status/VerificationEnrollmentStatus.java index af728666f..d2c2ebcb9 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/type/verification/VerificationEnrollmentStatus.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/type/status/VerificationEnrollmentStatus.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.type.verification; +package me.whereareiam.identica.feature.verification.type.status; /** * Status for a verification method enrollment process. diff --git a/identica-api/src/main/java/me/whereareiam/identica/type/verification/status/VerificationResetStatus.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/type/status/VerificationResetStatus.java similarity index 74% rename from identica-api/src/main/java/me/whereareiam/identica/type/verification/status/VerificationResetStatus.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/type/status/VerificationResetStatus.java index 006e92b4e..002e861b7 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/type/verification/status/VerificationResetStatus.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/type/status/VerificationResetStatus.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.type.verification.status; +package me.whereareiam.identica.feature.verification.type.status; /** * Outcome states for verification reset operations. diff --git a/identica-api/src/main/java/me/whereareiam/identica/type/verification/VerificationResolutionStatus.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/type/status/VerificationResolutionStatus.java similarity index 72% rename from identica-api/src/main/java/me/whereareiam/identica/type/verification/VerificationResolutionStatus.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/type/status/VerificationResolutionStatus.java index 9150aa6ae..6efa9aa6d 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/type/verification/VerificationResolutionStatus.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/type/status/VerificationResolutionStatus.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.type.verification; +package me.whereareiam.identica.feature.verification.type.status; /** * Result status for resolving a provider verification requirement. diff --git a/identica-api/src/main/java/me/whereareiam/identica/type/verification/status/VerificationSelectionStatus.java b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/type/status/VerificationSelectionStatus.java similarity index 93% rename from identica-api/src/main/java/me/whereareiam/identica/type/verification/status/VerificationSelectionStatus.java rename to identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/type/status/VerificationSelectionStatus.java index 521a1060c..8c392de8d 100644 --- a/identica-api/src/main/java/me/whereareiam/identica/type/verification/status/VerificationSelectionStatus.java +++ b/identica-feature/feature-verification/verification-api/src/main/java/me/whereareiam/identica/feature/verification/type/status/VerificationSelectionStatus.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.type.verification.status; +package me.whereareiam.identica.feature.verification.type.status; /** * Outcome states for provider-to-method selection updates. diff --git a/identica-feature/feature-verification/verification-common/build.gradle.kts b/identica-feature/feature-verification/verification-common/build.gradle.kts new file mode 100644 index 000000000..84380330a --- /dev/null +++ b/identica-feature/feature-verification/verification-common/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + id("shared") +} + +dependencies { + implementation(projects.featureVerificationApi) + compileOnly(projects.identicaAdapterCommand) + compileOnly(libs.cloud.core) + + testImplementation(projects.identicaAdapterCommand) + testImplementation(libs.cloud.core) +} diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/verification/DefaultVerificationRegistry.java b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/DefaultVerificationRegistry.java similarity index 83% rename from identica-common/src/main/java/me/whereareiam/identica/common/verification/DefaultVerificationRegistry.java rename to identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/DefaultVerificationRegistry.java index a7f821977..c2cc3bd8b 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/verification/DefaultVerificationRegistry.java +++ b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/DefaultVerificationRegistry.java @@ -1,12 +1,10 @@ -package me.whereareiam.identica.common.verification; +package me.whereareiam.identica.feature.verification; import com.google.inject.Inject; import com.google.inject.Singleton; import me.whereareiam.identica.event.EventManager; -import me.whereareiam.identica.event.verification.VerificationMethodRegisteredEvent; -import me.whereareiam.identica.event.verification.VerificationMethodUnregisteredEvent; -import me.whereareiam.identica.verification.VerificationMethod; -import me.whereareiam.identica.verification.VerificationRegistry; +import me.whereareiam.identica.feature.verification.event.VerificationMethodRegisteredEvent; +import me.whereareiam.identica.feature.verification.event.VerificationMethodUnregisteredEvent; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/verification/DefaultVerificationService.java b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/DefaultVerificationService.java similarity index 77% rename from identica-common/src/main/java/me/whereareiam/identica/common/verification/DefaultVerificationService.java rename to identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/DefaultVerificationService.java index 8e082c67d..980594bc5 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/verification/DefaultVerificationService.java +++ b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/DefaultVerificationService.java @@ -1,41 +1,38 @@ -package me.whereareiam.identica.common.verification; +package me.whereareiam.identica.feature.verification; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.common.verification.challenge.VerificationChallengeLifecycle; -import me.whereareiam.identica.common.verification.enrollment.VerificationEnrollmentLifecycle; -import me.whereareiam.identica.common.verification.enrollment.VerificationEnrollmentStore; -import me.whereareiam.identica.database.VerificationPersistenceService; import me.whereareiam.identica.event.EventManager; -import me.whereareiam.identica.event.verification.VerificationMethodDisabledEvent; -import me.whereareiam.identica.event.verification.VerificationResetEvent; -import me.whereareiam.identica.event.verification.VerificationSelectionEvent; +import me.whereareiam.identica.feature.verification.challenge.VerificationChallengeLifecycle; +import me.whereareiam.identica.feature.verification.database.VerificationPersistenceService; +import me.whereareiam.identica.feature.verification.enrollment.VerificationEnrollmentLifecycle; +import me.whereareiam.identica.feature.verification.enrollment.VerificationEnrollmentStore; +import me.whereareiam.identica.feature.verification.event.VerificationMethodDisabledEvent; +import me.whereareiam.identica.feature.verification.event.VerificationResetEvent; +import me.whereareiam.identica.feature.verification.event.VerificationSelectionEvent; +import me.whereareiam.identica.feature.verification.model.VerificationDisableResult; +import me.whereareiam.identica.feature.verification.model.VerificationResetResult; +import me.whereareiam.identica.feature.verification.model.challenge.VerificationChallengeResult; +import me.whereareiam.identica.feature.verification.model.config.VerificationSettings; +import me.whereareiam.identica.feature.verification.model.enrollment.PendingVerificationEnrollment; +import me.whereareiam.identica.feature.verification.model.enrollment.VerificationEnrollment; +import me.whereareiam.identica.feature.verification.model.enrollment.VerificationEnrollmentResult; +import me.whereareiam.identica.feature.verification.model.resolution.VerificationResolutionRequest; +import me.whereareiam.identica.feature.verification.model.resolution.VerificationResolutionResult; +import me.whereareiam.identica.feature.verification.model.selection.VerificationSelection; +import me.whereareiam.identica.feature.verification.model.selection.VerificationSelectionResult; +import me.whereareiam.identica.feature.verification.resolution.VerificationRequirementResolver; +import me.whereareiam.identica.feature.verification.type.UnavailableSelectionPolicy; +import me.whereareiam.identica.feature.verification.type.status.VerificationDisableStatus; +import me.whereareiam.identica.feature.verification.type.status.VerificationEnrollmentStatus; +import me.whereareiam.identica.feature.verification.type.status.VerificationResetStatus; +import me.whereareiam.identica.feature.verification.type.status.VerificationSelectionStatus; import me.whereareiam.identica.identity.session.SessionService; import me.whereareiam.identica.model.Session; -import me.whereareiam.identica.model.config.Verification; -import me.whereareiam.identica.model.verification.VerificationDisableResult; -import me.whereareiam.identica.model.verification.VerificationResolutionRequest; -import me.whereareiam.identica.model.verification.VerificationResolutionResult; -import me.whereareiam.identica.model.verification.VerificationResetResult; -import me.whereareiam.identica.model.verification.challenge.VerificationChallengeResult; -import me.whereareiam.identica.model.verification.enrollment.VerificationEnrollment; -import me.whereareiam.identica.model.verification.enrollment.PendingVerificationEnrollment; -import me.whereareiam.identica.model.verification.enrollment.VerificationEnrollmentResult; -import me.whereareiam.identica.model.verification.selection.VerificationSelection; -import me.whereareiam.identica.model.verification.selection.VerificationSelectionResult; -import me.whereareiam.identica.common.verification.resolution.VerificationRequirementResolver; import me.whereareiam.identica.provider.ProviderManager; -import me.whereareiam.identica.type.provider.ProviderCapability; -import me.whereareiam.identica.type.verification.UnavailableSelectionPolicy; -import me.whereareiam.identica.type.verification.VerificationEnrollmentStatus; -import me.whereareiam.identica.type.verification.status.VerificationDisableStatus; -import me.whereareiam.identica.type.verification.status.VerificationResetStatus; -import me.whereareiam.identica.type.verification.status.VerificationSelectionStatus; -import me.whereareiam.identica.verification.VerificationInteraction; -import me.whereareiam.identica.verification.VerificationRegistry; -import me.whereareiam.identica.verification.VerificationService; +import me.whereareiam.identica.type.provider.capability.ProviderCapability; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -45,9 +42,11 @@ @Singleton @RequiredArgsConstructor(onConstructor_ = @Inject) public class DefaultVerificationService implements VerificationService { + // TODO Migrate and use capbility api class + private static final ProviderCapability VERIFICATION_CAPABILITY = ProviderCapability.of("verification"); private final VerificationPersistenceService persistenceService; private final VerificationEnrollmentStore enrollmentStore; - private final Provider verificationProvider; + private final Provider verificationProvider; private final VerificationRegistry methodRegistry; private final ProviderManager providerManager; private final VerificationPolicyResolver policyResolver; @@ -259,6 +258,6 @@ private boolean supportsVerification(@Nullable String providerId) { .anyMatch(provider -> provider != null && provider.getDescriptor() != null && providerId.equalsIgnoreCase(provider.getDescriptor().getId()) - && provider.getDescriptor().hasCapability(ProviderCapability.VERIFICATION)); + && provider.getDescriptor().hasCapability(VERIFICATION_CAPABILITY)); } } diff --git a/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/VerificationCommonConfiguration.java b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/VerificationCommonConfiguration.java new file mode 100644 index 000000000..9bd60b550 --- /dev/null +++ b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/VerificationCommonConfiguration.java @@ -0,0 +1,56 @@ +package me.whereareiam.identica.feature.verification; + +import com.google.inject.AbstractModule; +import com.google.inject.Inject; +import com.google.inject.TypeLiteral; +import com.google.inject.multibindings.Multibinder; +import com.google.inject.name.Names; +import me.whereareiam.identica.feature.verification.challenge.VerificationChallengeStore; +import me.whereareiam.identica.feature.verification.command.VerificationCommandRegistrar; +import me.whereareiam.identica.feature.verification.command.executor.VerificationCommand; +import me.whereareiam.identica.feature.verification.command.executor.VerificationConfirmCommand; +import me.whereareiam.identica.feature.verification.command.executor.VerificationEnrollmentCommand; +import me.whereareiam.identica.feature.verification.command.executor.VerificationSelectionCommand; +import me.whereareiam.identica.feature.verification.command.executor.admin.VerificationResetCommand; +import me.whereareiam.identica.feature.verification.config.VerificationCommands; +import me.whereareiam.identica.feature.verification.config.VerificationMessages; +import me.whereareiam.identica.feature.verification.config.provider.VerificationCommandsProvider; +import me.whereareiam.identica.feature.verification.config.provider.VerificationMessagesProvider; +import me.whereareiam.identica.feature.verification.config.provider.VerificationSettingsProvider; +import me.whereareiam.identica.feature.verification.enrollment.VerificationEnrollmentStore; +import me.whereareiam.identica.feature.verification.model.config.VerificationSettings; +import me.whereareiam.identica.feature.verification.type.totp.TotpVerificationMethod; + +public class VerificationCommonConfiguration extends AbstractModule { + @Override + protected void configure() { + bind(VerificationCommands.class).toProvider(VerificationCommandsProvider.class); + bind(VerificationMessages.class).toProvider(VerificationMessagesProvider.class); + bind(VerificationSettings.class).toProvider(VerificationSettingsProvider.class); + bind(VerificationEnrollmentStore.class).asEagerSingleton(); + bind(VerificationChallengeStore.class).asEagerSingleton(); + bind(VerificationService.class).to(DefaultVerificationService.class).asEagerSingleton(); + bind(VerificationRegistry.class).to(DefaultVerificationRegistry.class).asEagerSingleton(); + Multibinder.newSetBinder(binder(), VerificationMethod.class) + .addBinding() + .to(TotpVerificationMethod.class); + + Multibinder commands = Multibinder.newSetBinder( + binder(), + new TypeLiteral<>() {}, + Names.named("verificationCommandInstances") + ); + commands.addBinding().to(VerificationCommand.class); + commands.addBinding().to(VerificationEnrollmentCommand.class); + commands.addBinding().to(VerificationConfirmCommand.class); + commands.addBinding().to(VerificationSelectionCommand.class); + commands.addBinding().to(VerificationResetCommand.class); + + bind(VerificationCommandRegistrar.class).asEagerSingleton(); + } + + @Inject + void registerVerificationCommands(VerificationCommandRegistrar registrar) { + registrar.registerCommands(); + } +} diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/verification/VerificationPolicyResolver.java b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/VerificationPolicyResolver.java similarity index 77% rename from identica-common/src/main/java/me/whereareiam/identica/common/verification/VerificationPolicyResolver.java rename to identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/VerificationPolicyResolver.java index 9003e11d8..31056972a 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/verification/VerificationPolicyResolver.java +++ b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/VerificationPolicyResolver.java @@ -1,17 +1,18 @@ -package me.whereareiam.identica.common.verification; +package me.whereareiam.identica.feature.verification; import com.google.inject.Inject; -import com.google.inject.Provider; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; +import me.whereareiam.identica.feature.verification.config.provider.VerificationProvidersProvider; +import me.whereareiam.identica.feature.verification.model.config.VerificationProviders; +import me.whereareiam.identica.feature.verification.type.UnavailableSelectionPolicy; import me.whereareiam.identica.model.config.provider.Providers; -import me.whereareiam.identica.type.verification.UnavailableSelectionPolicy; import org.jetbrains.annotations.Nullable; @Singleton @RequiredArgsConstructor(onConstructor_ = @Inject) public class VerificationPolicyResolver { - private final Provider providersProvider; + private final VerificationProvidersProvider providersProvider; public @Nullable ResolvedProviderPolicy resolveProviderPolicy(@Nullable String providerId) { if (providerId == null || providerId.isBlank()) return null; @@ -23,7 +24,7 @@ public class VerificationPolicyResolver { if (entry == null) continue; if (!providerId.equalsIgnoreCase(entry.getId())) continue; - Providers.ProviderEntry.Verification verification = entry.getVerification(); + VerificationProviders.Verification verification = verification(entry); if (verification == null) { return new ResolvedProviderPolicy( false, @@ -68,9 +69,9 @@ public boolean hasConfiguredProvider(@Nullable String providerId) { for (Providers.ProviderEntry entry : providers.getProviders()) { if (entry == null || !providerId.equalsIgnoreCase(entry.getId())) continue; - Providers.ProviderEntry.Verification verification = entry.getVerification(); + VerificationProviders.Verification verification = verification(entry); if (verification == null) return null; - for (Providers.ProviderEntry.Verification.MethodEntry method : verification.getMethods()) { + for (VerificationProviders.Verification.MethodEntry method : verification.getMethods()) { if (method == null || !methodId.equalsIgnoreCase(method.getId())) continue; boolean required = method.getRequired() != null ? method.getRequired() : verification.isRequired(); @@ -87,6 +88,12 @@ public boolean hasConfiguredProvider(@Nullable String providerId) { return null; } + private @Nullable VerificationProviders.Verification verification(@Nullable Providers.ProviderEntry entry) { + if (entry == null || !(entry.getFeatures() instanceof VerificationProviders features)) return null; + + return features.getVerification(); + } + public record ResolvedProviderPolicy( boolean enabled, boolean required, diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/verification/challenge/VerificationChallengeLifecycle.java b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/challenge/VerificationChallengeLifecycle.java similarity index 81% rename from identica-common/src/main/java/me/whereareiam/identica/common/verification/challenge/VerificationChallengeLifecycle.java rename to identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/challenge/VerificationChallengeLifecycle.java index b7acb1c08..d1f0b37c9 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/verification/challenge/VerificationChallengeLifecycle.java +++ b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/challenge/VerificationChallengeLifecycle.java @@ -1,26 +1,25 @@ -package me.whereareiam.identica.common.verification.challenge; +package me.whereareiam.identica.feature.verification.challenge; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.common.verification.codec.VerificationStateCodec; -import me.whereareiam.identica.database.VerificationPersistenceService; import me.whereareiam.identica.event.EventManager; -import me.whereareiam.identica.event.verification.challenge.VerificationChallengeFailedEvent; -import me.whereareiam.identica.event.verification.challenge.VerificationChallengeStartedEvent; -import me.whereareiam.identica.event.verification.challenge.VerificationChallengeSucceededEvent; -import me.whereareiam.identica.model.config.Verification; -import me.whereareiam.identica.model.verification.challenge.PendingVerificationChallenge; -import me.whereareiam.identica.model.verification.challenge.VerificationChallengeContext; -import me.whereareiam.identica.model.verification.challenge.VerificationChallengeResult; -import me.whereareiam.identica.model.verification.challenge.VerificationChallengeState; -import me.whereareiam.identica.model.verification.enrollment.VerificationEnrollment; -import me.whereareiam.identica.type.verification.VerificationChallengeStatus; -import me.whereareiam.identica.verification.VerificationInteraction; -import me.whereareiam.identica.verification.VerificationMethod; -import me.whereareiam.identica.verification.VerificationRegistry; -import me.whereareiam.identica.verification.process.VerificationChallengeProcess; +import me.whereareiam.identica.feature.verification.VerificationInteraction; +import me.whereareiam.identica.feature.verification.VerificationMethod; +import me.whereareiam.identica.feature.verification.VerificationRegistry; +import me.whereareiam.identica.feature.verification.database.VerificationPersistenceService; +import me.whereareiam.identica.feature.verification.event.challenge.VerificationChallengeFailedEvent; +import me.whereareiam.identica.feature.verification.event.challenge.VerificationChallengeStartedEvent; +import me.whereareiam.identica.feature.verification.event.challenge.VerificationChallengeSucceededEvent; +import me.whereareiam.identica.feature.verification.model.challenge.PendingVerificationChallenge; +import me.whereareiam.identica.feature.verification.model.challenge.VerificationChallengeContext; +import me.whereareiam.identica.feature.verification.model.challenge.VerificationChallengeResult; +import me.whereareiam.identica.feature.verification.model.config.VerificationSettings; +import me.whereareiam.identica.feature.verification.model.enrollment.VerificationEnrollment; +import me.whereareiam.identica.feature.verification.process.VerificationChallengeProcess; +import me.whereareiam.identica.feature.verification.state.VerificationChallengeState; +import me.whereareiam.identica.feature.verification.type.status.VerificationChallengeStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -34,7 +33,7 @@ public class VerificationChallengeLifecycle { private final VerificationChallengeStore challengeStore; private final VerificationStateCodec stateCodec; private final VerificationRegistry methodRegistry; - private final Provider verificationProvider; + private final Provider verificationProvider; private final EventManager eventManager; public @NotNull VerificationChallengeResult submitChallengeInteraction( diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/verification/challenge/VerificationChallengeStore.java b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/challenge/VerificationChallengeStore.java similarity index 88% rename from identica-common/src/main/java/me/whereareiam/identica/common/verification/challenge/VerificationChallengeStore.java rename to identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/challenge/VerificationChallengeStore.java index 7a001b9ef..6b5b4d58f 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/verification/challenge/VerificationChallengeStore.java +++ b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/challenge/VerificationChallengeStore.java @@ -1,10 +1,10 @@ -package me.whereareiam.identica.common.verification.challenge; +package me.whereareiam.identica.feature.verification.challenge; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; -import me.whereareiam.identica.model.config.Verification; -import me.whereareiam.identica.model.verification.challenge.PendingVerificationChallenge; +import me.whereareiam.identica.feature.verification.model.challenge.PendingVerificationChallenge; +import me.whereareiam.identica.feature.verification.model.config.VerificationSettings; import me.whereareiam.identica.replication.ReplicationSystem; import me.whereareiam.identica.replication.cache.LocalCache; import org.jetbrains.annotations.NotNull; @@ -22,12 +22,12 @@ public class VerificationChallengeStore { private final LocalCache records; private final LocalCache active; private final LocalCache verified; - private final Provider verificationProvider; + private final Provider verificationProvider; @Inject public VerificationChallengeStore( @NotNull ReplicationSystem replicationSystem, - @NotNull Provider verificationProvider + @NotNull Provider verificationProvider ) { this.records = replicationSystem.cache(RECORD_NAMESPACE).local(); this.active = replicationSystem.cache(ACTIVE_NAMESPACE).local(); diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/verification/codec/VerificationStateCodec.java b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/challenge/VerificationStateCodec.java similarity index 93% rename from identica-common/src/main/java/me/whereareiam/identica/common/verification/codec/VerificationStateCodec.java rename to identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/challenge/VerificationStateCodec.java index 6a75437c9..ad3b01bf3 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/verification/codec/VerificationStateCodec.java +++ b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/challenge/VerificationStateCodec.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.common.verification.codec; +package me.whereareiam.identica.feature.verification.challenge; import me.whereareiam.identica.pipeline.state.PipelineStateItem; import me.whereareiam.identica.replication.codec.SnapshotCodec; diff --git a/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/command/VerificationCommandRegistrar.java b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/command/VerificationCommandRegistrar.java new file mode 100644 index 000000000..1c2f76ef5 --- /dev/null +++ b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/command/VerificationCommandRegistrar.java @@ -0,0 +1,36 @@ +package me.whereareiam.identica.feature.verification.command; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.google.inject.name.Named; +import me.whereareiam.identica.command.CommandService; +import me.whereareiam.identica.feature.verification.command.suggestion.VerificationMethodSuggestions; +import me.whereareiam.identica.feature.verification.config.VerificationCommands; + +import java.util.Set; + +@Singleton +public class VerificationCommandRegistrar { + private final CommandService commandService; + private final VerificationMethodSuggestions verificationMethodSuggestions; + private final VerificationCommands verificationCommands; + private final Set commandInstances; + + @Inject + public VerificationCommandRegistrar( + CommandService commandService, + VerificationMethodSuggestions verificationMethodSuggestions, + VerificationCommands verificationCommands, + @Named("verificationCommandInstances") Set commandInstances + ) { + this.commandService = commandService; + this.verificationMethodSuggestions = verificationMethodSuggestions; + this.verificationCommands = verificationCommands; + this.commandInstances = commandInstances; + } + + public void registerCommands() { + commandService.registerSuggestionProvider(VerificationMethodSuggestions.KEY, verificationMethodSuggestions); + commandService.registerCommandInstances(verificationCommands.getCommands(), commandInstances.toArray()); + } +} diff --git a/identica-adapter-command/src/main/java/me/whereareiam/identica/adapter/command/executor/verification/VerificationCommand.java b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/command/executor/VerificationCommand.java similarity index 77% rename from identica-adapter-command/src/main/java/me/whereareiam/identica/adapter/command/executor/verification/VerificationCommand.java rename to identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/command/executor/VerificationCommand.java index 817fba5fb..76099a8c2 100644 --- a/identica-adapter-command/src/main/java/me/whereareiam/identica/adapter/command/executor/verification/VerificationCommand.java +++ b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/command/executor/VerificationCommand.java @@ -1,21 +1,21 @@ -package me.whereareiam.identica.adapter.command.executor.verification; +package me.whereareiam.identica.feature.verification.command.executor; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; -import lombok.RequiredArgsConstructor; import me.whereareiam.identica.Serializer; import me.whereareiam.identica.annotation.Command; import me.whereareiam.identica.annotation.Definition; import me.whereareiam.identica.command.SessionBoundCommand; +import me.whereareiam.identica.feature.verification.VerificationRegistry; +import me.whereareiam.identica.feature.verification.VerificationService; +import me.whereareiam.identica.feature.verification.config.VerificationMessages; +import me.whereareiam.identica.feature.verification.model.enrollment.VerificationEnrollment; +import me.whereareiam.identica.feature.verification.model.selection.VerificationSelection; import me.whereareiam.identica.identity.actor.Identity; import me.whereareiam.identica.identity.session.SessionService; -import me.whereareiam.identica.model.config.type.DateTimePattern; import me.whereareiam.identica.model.config.Messages; -import me.whereareiam.identica.model.verification.enrollment.VerificationEnrollment; -import me.whereareiam.identica.model.verification.selection.VerificationSelection; -import me.whereareiam.identica.verification.VerificationRegistry; -import me.whereareiam.identica.verification.VerificationService; +import me.whereareiam.identica.model.config.type.DateTimePattern; import me.whereareiam.keystone.Actor; import me.whereareiam.keystone.model.SerializerContent; import me.whereareiam.keystone.model.SerializerOptions; @@ -28,15 +28,30 @@ import java.util.*; @Singleton -@RequiredArgsConstructor(onConstructor_ = @Inject) public class VerificationCommand extends SessionBoundCommand { private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(ZoneId.systemDefault()); - private final Provider messagesProvider; + private final Provider coreMessagesProvider; + private final Provider messagesProvider; private final VerificationService verificationService; private final VerificationRegistry verificationRegistry; private final SessionService sessionService; + @Inject + public VerificationCommand( + Provider coreMessagesProvider, + Provider messagesProvider, + VerificationService verificationService, + VerificationRegistry verificationRegistry, + SessionService sessionService + ) { + this.coreMessagesProvider = coreMessagesProvider; + this.messagesProvider = messagesProvider; + this.verificationService = verificationService; + this.verificationRegistry = verificationRegistry; + this.sessionService = sessionService; + } + @Override protected @NotNull SessionService sessionService() { return sessionService; @@ -44,7 +59,7 @@ public class VerificationCommand extends SessionBoundCommand { @Override protected @Nullable String currentSessionRequiredMessage() { - return messagesProvider.get().getCommands().getCurrentSessionRequired(); + return coreMessagesProvider.get().getCommands().getCurrentSessionRequired(); } @Definition("verification") @@ -62,8 +77,8 @@ public void status(@NotNull Actor sender) { var accountUniqueId = requireAccountUniqueId(identity); if (accountUniqueId == null) return; - Messages.Commands.Verification messages = verificationMessages(); - Messages.Commands.Verification.Status statusMessages = messages.getStatus(); + VerificationMessages.Commands messages = verificationMessages(); + VerificationMessages.Commands.Status statusMessages = messages.getStatus(); List enrollments = verificationService.findEnrollments(accountUniqueId); List selections = verificationService.findSelections(accountUniqueId); @@ -78,7 +93,7 @@ public void status(@NotNull Actor sender) { } private List buildEnrollmentLines( - Messages.Commands.Verification.Status messages, + VerificationMessages.Commands.Status messages, List enrollments ) { if (enrollments == null || enrollments.isEmpty()) return List.of(messages.getEmptyEnrollments()); @@ -94,7 +109,7 @@ private List buildEnrollmentLines( } private List buildSelectionLines( - Messages.Commands.Verification.Status messages, + VerificationMessages.Commands.Status messages, List selections ) { if (selections == null || selections.isEmpty()) return List.of(messages.getEmptySelections()); @@ -109,7 +124,7 @@ private List buildSelectionLines( return lines; } - private String formatEntry(Messages.Commands.EntryFormat format, Map placeholders) { + private String formatEntry(VerificationMessages.Commands.EntryFormat format, Map placeholders) { boolean complete = placeholders.values().stream().allMatch(value -> value != null && !value.isBlank()); String result = complete ? format.getFormat() : format.getEmptyFormat(); SerializerOptions.PlaceholderFormat placeholderFormat = placeholderFormat(); @@ -120,8 +135,8 @@ private String formatEntry(Messages.Commands.EntryFormat format, Map { - private final Provider messagesProvider; - private final Provider verificationProvider; + private final Provider coreMessagesProvider; + private final Provider messagesProvider; + private final Provider verificationProvider; private final VerificationService verificationService; private final PipelineStateStore pipelineStateStore; private final ConnectionCoordinator connectionCoordinator; @@ -45,8 +47,9 @@ public class VerificationConfirmCommand extends ProtectedActionCommand { @Inject public VerificationConfirmCommand( - Provider messagesProvider, - Provider verificationProvider, + Provider coreMessagesProvider, + Provider messagesProvider, + Provider verificationProvider, VerificationService verificationService, PipelineStateStore pipelineStateStore, ConnectionCoordinator connectionCoordinator, @@ -54,6 +57,7 @@ public VerificationConfirmCommand( SessionService sessionService ) { super(verificationService); + this.coreMessagesProvider = coreMessagesProvider; this.messagesProvider = messagesProvider; this.verificationProvider = verificationProvider; this.verificationService = verificationService; @@ -70,7 +74,7 @@ public VerificationConfirmCommand( @Override protected @Nullable String currentSessionRequiredMessage() { - return messagesProvider.get().getCommands().getCurrentSessionRequired(); + return coreMessagesProvider.get().getCommands().getCurrentSessionRequired(); } @Definition("verification-confirm") @@ -185,8 +189,8 @@ private boolean submitChallenge(@NotNull Identity identity, @NotNull String inpu return state.getScenario().getProvider().getProviderId(); } - private Messages.Commands.Verification verificationMessages() { - return messagesProvider.get().getCommands().getVerification(); + private VerificationMessages.Commands verificationMessages() { + return messagesProvider.get().getCommands(); } private void sendMessage(@NotNull Actor sender, @Nullable String message, @NotNull Map placeholders) { diff --git a/identica-adapter-command/src/main/java/me/whereareiam/identica/adapter/command/executor/verification/VerificationEnrollmentCommand.java b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/command/executor/VerificationEnrollmentCommand.java similarity index 78% rename from identica-adapter-command/src/main/java/me/whereareiam/identica/adapter/command/executor/verification/VerificationEnrollmentCommand.java rename to identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/command/executor/VerificationEnrollmentCommand.java index 773e74131..07e7f4dee 100644 --- a/identica-adapter-command/src/main/java/me/whereareiam/identica/adapter/command/executor/verification/VerificationEnrollmentCommand.java +++ b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/command/executor/VerificationEnrollmentCommand.java @@ -1,23 +1,24 @@ -package me.whereareiam.identica.adapter.command.executor.verification; +package me.whereareiam.identica.feature.verification.command.executor; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import me.whereareiam.identica.Serializer; -import me.whereareiam.identica.adapter.command.suggestion.VerificationMethodSuggestions; import me.whereareiam.identica.annotation.Argument; import me.whereareiam.identica.annotation.Command; import me.whereareiam.identica.annotation.Definition; import me.whereareiam.identica.annotation.Suggestions; -import me.whereareiam.identica.command.ProtectedActionCommand; +import me.whereareiam.identica.feature.verification.VerificationInteraction; +import me.whereareiam.identica.feature.verification.VerificationService; +import me.whereareiam.identica.feature.verification.command.ProtectedActionCommand; +import me.whereareiam.identica.feature.verification.command.suggestion.VerificationMethodSuggestions; +import me.whereareiam.identica.feature.verification.config.VerificationMessages; +import me.whereareiam.identica.feature.verification.model.interaction.CodeVerificationInteraction; +import me.whereareiam.identica.feature.verification.model.interaction.SavedVerificationInteraction; import me.whereareiam.identica.identity.actor.Identity; import me.whereareiam.identica.identity.session.SessionService; import me.whereareiam.identica.model.Session; import me.whereareiam.identica.model.config.Messages; -import me.whereareiam.identica.model.verification.interaction.CodeVerificationInteraction; -import me.whereareiam.identica.model.verification.interaction.SavedVerificationInteraction; -import me.whereareiam.identica.verification.VerificationInteraction; -import me.whereareiam.identica.verification.VerificationService; import me.whereareiam.keystone.Actor; import me.whereareiam.keystone.model.SerializerContent; import org.jetbrains.annotations.NotNull; @@ -27,19 +28,22 @@ @Singleton public class VerificationEnrollmentCommand extends ProtectedActionCommand { - private final Provider messagesProvider; + private final Provider coreMessagesProvider; + private final Provider messagesProvider; private final VerificationService verificationService; private final VerificationResultRenderer resultRenderer; private final SessionService sessionService; @Inject public VerificationEnrollmentCommand( - Provider messagesProvider, + Provider coreMessagesProvider, + Provider messagesProvider, VerificationService verificationService, VerificationResultRenderer resultRenderer, SessionService sessionService ) { super(verificationService); + this.coreMessagesProvider = coreMessagesProvider; this.messagesProvider = messagesProvider; this.verificationService = verificationService; this.resultRenderer = resultRenderer; @@ -53,7 +57,7 @@ public VerificationEnrollmentCommand( @Override protected @Nullable String currentSessionRequiredMessage() { - return messagesProvider.get().getCommands().getCurrentSessionRequired(); + return coreMessagesProvider.get().getCommands().getCurrentSessionRequired(); } @Definition("verification-enroll") @@ -107,8 +111,8 @@ public void cancel(@NotNull Actor sender) { sendMessage(sender, verificationMessages().getCancel().getNoPending(), Map.of()); } - private Messages.Commands.Verification verificationMessages() { - return messagesProvider.get().getCommands().getVerification(); + private VerificationMessages.Commands verificationMessages() { + return messagesProvider.get().getCommands(); } private VerificationInteraction interaction(@NotNull java.util.UUID uniqueId, @NotNull String input) { diff --git a/identica-adapter-command/src/main/java/me/whereareiam/identica/adapter/command/executor/verification/VerificationResultRenderer.java b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/command/executor/VerificationResultRenderer.java similarity index 81% rename from identica-adapter-command/src/main/java/me/whereareiam/identica/adapter/command/executor/verification/VerificationResultRenderer.java rename to identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/command/executor/VerificationResultRenderer.java index a0287416d..2c2bd7676 100644 --- a/identica-adapter-command/src/main/java/me/whereareiam/identica/adapter/command/executor/verification/VerificationResultRenderer.java +++ b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/command/executor/VerificationResultRenderer.java @@ -1,20 +1,19 @@ -package me.whereareiam.identica.adapter.command.executor.verification; +package me.whereareiam.identica.feature.verification.command.executor; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; -import lombok.RequiredArgsConstructor; import me.whereareiam.identica.Serializer; +import me.whereareiam.identica.feature.verification.VerificationRegistry; +import me.whereareiam.identica.feature.verification.config.VerificationMessages; +import me.whereareiam.identica.feature.verification.model.VerificationDisableResult; +import me.whereareiam.identica.feature.verification.model.enrollment.VerificationEnrollmentResult; +import me.whereareiam.identica.feature.verification.model.process.VerificationProcessDisplay; +import me.whereareiam.identica.feature.verification.model.selection.VerificationSelectionResult; +import me.whereareiam.identica.feature.verification.type.status.VerificationDisableStatus; +import me.whereareiam.identica.feature.verification.type.status.VerificationEnrollmentStatus; +import me.whereareiam.identica.feature.verification.type.status.VerificationSelectionStatus; import me.whereareiam.identica.logging.Logger; -import me.whereareiam.identica.model.config.Messages; -import me.whereareiam.identica.model.verification.VerificationDisableResult; -import me.whereareiam.identica.model.verification.enrollment.VerificationEnrollmentResult; -import me.whereareiam.identica.model.verification.process.VerificationProcessDisplay; -import me.whereareiam.identica.model.verification.selection.VerificationSelectionResult; -import me.whereareiam.identica.type.verification.VerificationEnrollmentStatus; -import me.whereareiam.identica.type.verification.status.VerificationDisableStatus; -import me.whereareiam.identica.type.verification.status.VerificationSelectionStatus; -import me.whereareiam.identica.verification.VerificationRegistry; import me.whereareiam.keystone.Actor; import me.whereareiam.keystone.model.SerializerContent; import me.whereareiam.keystone.model.SerializerOptions; @@ -26,15 +25,23 @@ import java.util.*; @Singleton -@RequiredArgsConstructor(onConstructor_ = @Inject) public class VerificationResultRenderer { - private final Provider messagesProvider; + private final Provider messagesProvider; private final VerificationRegistry verificationRegistry; + @Inject + public VerificationResultRenderer( + Provider messagesProvider, + VerificationRegistry verificationRegistry + ) { + this.messagesProvider = messagesProvider; + this.verificationRegistry = verificationRegistry; + } + public void presentEnrollmentResult(@NotNull Actor sender, @NotNull VerificationEnrollmentResult result) { - Messages.Commands.Verification messages = verificationMessages(); - Messages.Commands.Verification.Enroll enrollMessages = messages.getEnroll(); - Messages.Commands.Verification.Confirm confirmMessages = messages.getConfirm(); + VerificationMessages.Commands messages = verificationMessages(); + VerificationMessages.Commands.Enroll enrollMessages = messages.getEnroll(); + VerificationMessages.Commands.Confirm confirmMessages = messages.getConfirm(); VerificationEnrollmentStatus status = result.getStatus(); if (status == null) return; @@ -65,8 +72,8 @@ public void presentEnrollmentResult(@NotNull Actor sender, @NotNull Verification } public void presentSelectionResult(@NotNull Actor sender, @NotNull VerificationSelectionResult result) { - Messages.Commands.Verification messages = verificationMessages(); - Messages.Commands.Verification.Use useMessages = messages.getUse(); + VerificationMessages.Commands messages = verificationMessages(); + VerificationMessages.Commands.Use useMessages = messages.getUse(); VerificationSelectionStatus status = result.getStatus(); if (status == null) return; @@ -83,7 +90,7 @@ public void presentSelectionResult(@NotNull Actor sender, @NotNull VerificationS } public void presentDisableResult(@NotNull Actor sender, @NotNull VerificationDisableResult result) { - Messages.Commands.Verification.Disable disableMessages = verificationMessages().getDisable(); + VerificationMessages.Commands.Disable disableMessages = verificationMessages().getDisable(); VerificationDisableStatus status = result.getStatus(); if (status == null) return; @@ -98,7 +105,7 @@ public void presentDisablePrompt(@NotNull Actor sender, @Nullable String methodI } private void sendRecoveryCodes(@NotNull Actor sender, @Nullable List recoveryCodes) { - Messages.Commands.Verification.Confirm.RecoveryCodes messages = verificationMessages().getConfirm().getRecoveryCodes(); + VerificationMessages.Commands.Confirm.RecoveryCodes messages = verificationMessages().getConfirm().getRecoveryCodes(); List entries = buildRecoveryCodeLines(messages, recoveryCodes); sendLines(sender, messages.getBody(), Map.of( "entries", String.join("\n", entries) @@ -106,7 +113,7 @@ private void sendRecoveryCodes(@NotNull Actor sender, @Nullable List rec } private List buildRecoveryCodeLines( - Messages.Commands.Verification.Confirm.RecoveryCodes messages, + VerificationMessages.Commands.Confirm.RecoveryCodes messages, @Nullable List recoveryCodes ) { if (recoveryCodes == null || recoveryCodes.isEmpty()) @@ -119,10 +126,10 @@ private List buildRecoveryCodeLines( entries.add(formatEntry(messages.getSingleColumnEntry(), placeholders)); } - if (messages.getLayout() == Messages.Commands.Verification.Confirm.RecoveryCodes.Layout.SINGLE_COLUMN) + if (messages.getLayout() == VerificationMessages.Commands.Confirm.RecoveryCodes.Layout.SINGLE_COLUMN) return entries; - Messages.Commands.EntryFormat twoColumnEntry = messages.getTwoColumnEntry(); + VerificationMessages.Commands.EntryFormat twoColumnEntry = messages.getTwoColumnEntry(); if (twoColumnEntry.getFormat().isBlank()) return entries; @@ -137,7 +144,7 @@ private List buildRecoveryCodeLines( return rows; } - private String formatEntry(Messages.Commands.EntryFormat format, Map placeholders) { + private String formatEntry(VerificationMessages.Commands.EntryFormat format, Map placeholders) { boolean complete = placeholders.values().stream().allMatch(value -> value != null && !value.isBlank()); String result = complete ? format.getFormat() : format.getEmptyFormat(); SerializerOptions.PlaceholderFormat placeholderFormat = placeholderFormat(); @@ -148,8 +155,8 @@ private String formatEntry(Messages.Commands.EntryFormat format, Map { - private final Provider messagesProvider; - private final Provider verificationProvider; + private final Provider coreMessagesProvider; + private final Provider messagesProvider; + private final Provider verificationProvider; private final VerificationService verificationService; private final VerificationResultRenderer resultRenderer; private final PipelineStateStore pipelineStateStore; @@ -36,14 +38,16 @@ public class VerificationSelectionCommand extends ProtectedActionCommand { @Inject public VerificationSelectionCommand( - Provider messagesProvider, - Provider verificationProvider, + Provider coreMessagesProvider, + Provider messagesProvider, + Provider verificationProvider, VerificationService verificationService, VerificationResultRenderer resultRenderer, PipelineStateStore pipelineStateStore, SessionService sessionService ) { super(verificationService); + this.coreMessagesProvider = coreMessagesProvider; this.messagesProvider = messagesProvider; this.verificationProvider = verificationProvider; this.verificationService = verificationService; @@ -59,7 +63,7 @@ public VerificationSelectionCommand( @Override protected @Nullable String currentSessionRequiredMessage() { - return messagesProvider.get().getCommands().getCurrentSessionRequired(); + return coreMessagesProvider.get().getCommands().getCurrentSessionRequired(); } @Definition("verification-use") @@ -69,7 +73,7 @@ public void use( @Argument("provider") @Suggestions(ProviderIdSuggestions.KEY) String providerId, @Argument("method") @Suggestions(VerificationMethodSuggestions.KEY) String methodId ) { - Identity identity = requireIdentity(sender, messagesProvider.get().getCommands().getVerification().getPlayerOnly()); + Identity identity = requireIdentity(sender, messagesProvider.get().getCommands().getPlayerOnly()); if (identity == null) return; if (requireCurrentSession(identity) == null) return; var accountUniqueId = requireAccountUniqueId(identity); @@ -84,7 +88,7 @@ public void disable( @NotNull Actor sender, @Argument("method") @Suggestions(VerificationMethodSuggestions.KEY) String methodId ) { - Identity identity = requireIdentity(sender, messagesProvider.get().getCommands().getVerification().getPlayerOnly()); + Identity identity = requireIdentity(sender, messagesProvider.get().getCommands().getPlayerOnly()); if (identity == null) return; if (requireCurrentSession(identity) == null) return; var accountUniqueId = requireAccountUniqueId(identity); diff --git a/identica-adapter-command/src/main/java/me/whereareiam/identica/adapter/command/executor/admin/VerificationResetCommand.java b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/command/executor/admin/VerificationResetCommand.java similarity index 76% rename from identica-adapter-command/src/main/java/me/whereareiam/identica/adapter/command/executor/admin/VerificationResetCommand.java rename to identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/command/executor/admin/VerificationResetCommand.java index 485d77506..09df611be 100644 --- a/identica-adapter-command/src/main/java/me/whereareiam/identica/adapter/command/executor/admin/VerificationResetCommand.java +++ b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/command/executor/admin/VerificationResetCommand.java @@ -1,9 +1,8 @@ -package me.whereareiam.identica.adapter.command.executor.admin; +package me.whereareiam.identica.feature.verification.command.executor.admin; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; -import lombok.RequiredArgsConstructor; import me.whereareiam.identica.Serializer; import me.whereareiam.identica.adapter.command.suggestion.CrossPlayerSuggestions; import me.whereareiam.identica.annotation.Argument; @@ -11,13 +10,13 @@ import me.whereareiam.identica.annotation.Definition; import me.whereareiam.identica.annotation.Suggestions; import me.whereareiam.identica.database.AccountPersistenceService; +import me.whereareiam.identica.feature.verification.VerificationService; +import me.whereareiam.identica.feature.verification.config.VerificationMessages; +import me.whereareiam.identica.feature.verification.type.status.VerificationResetStatus; import me.whereareiam.identica.identity.IdentityService; import me.whereareiam.identica.identity.actor.Identity; -import me.whereareiam.identica.model.config.Messages; import me.whereareiam.identica.model.identity.Account; -import me.whereareiam.identica.type.verification.status.VerificationResetStatus; import me.whereareiam.identica.util.UniqueIdUtil; -import me.whereareiam.identica.verification.VerificationService; import me.whereareiam.keystone.Actor; import me.whereareiam.keystone.model.SerializerContent; import org.jetbrains.annotations.NotNull; @@ -29,13 +28,25 @@ import java.util.UUID; @Singleton -@RequiredArgsConstructor(onConstructor_ = @Inject) public class VerificationResetCommand { - private final Provider messagesProvider; + private final Provider messagesProvider; private final VerificationService verificationService; private final IdentityService identityService; private final AccountPersistenceService accountPersistenceService; + @Inject + public VerificationResetCommand( + Provider messagesProvider, + VerificationService verificationService, + IdentityService identityService, + AccountPersistenceService accountPersistenceService + ) { + this.messagesProvider = messagesProvider; + this.verificationService = verificationService; + this.identityService = identityService; + this.accountPersistenceService = accountPersistenceService; + } + @Definition("admin-verification-reset") @Command("identica admin 2fa reset [provider]") public void reset( @@ -69,8 +80,8 @@ public void reset( return new ResolvedTarget(account.getUniqueId(), account.getUsername()); } - private Messages.Commands.Admin.Verification.Reset resetMessages() { - return messagesProvider.get().getCommands().getAdmin().getVerification().getReset(); + private VerificationMessages.Commands.Admin.Reset resetMessages() { + return messagesProvider.get().getCommands().getAdmin().getReset(); } private void sendMessage(@NotNull Actor sender, @Nullable String message, @NotNull Map placeholders) { diff --git a/identica-adapter-command/src/main/java/me/whereareiam/identica/adapter/command/suggestion/VerificationMethodSuggestions.java b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/command/suggestion/VerificationMethodSuggestions.java similarity index 81% rename from identica-adapter-command/src/main/java/me/whereareiam/identica/adapter/command/suggestion/VerificationMethodSuggestions.java rename to identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/command/suggestion/VerificationMethodSuggestions.java index bb8ec606c..0bc36fd0e 100644 --- a/identica-adapter-command/src/main/java/me/whereareiam/identica/adapter/command/suggestion/VerificationMethodSuggestions.java +++ b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/command/suggestion/VerificationMethodSuggestions.java @@ -1,10 +1,9 @@ -package me.whereareiam.identica.adapter.command.suggestion; +package me.whereareiam.identica.feature.verification.command.suggestion; import com.google.inject.Inject; import com.google.inject.Singleton; -import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.verification.VerificationMethod; -import me.whereareiam.identica.verification.VerificationRegistry; +import me.whereareiam.identica.feature.verification.VerificationMethod; +import me.whereareiam.identica.feature.verification.VerificationRegistry; import me.whereareiam.keystone.Actor; import org.incendo.cloud.context.CommandContext; import org.incendo.cloud.context.CommandInput; @@ -18,12 +17,16 @@ import java.util.concurrent.CompletableFuture; @Singleton -@RequiredArgsConstructor(onConstructor_ = @Inject) public class VerificationMethodSuggestions implements SuggestionProvider { public static final @NotNull String KEY = "verificationMethod"; private final @NotNull VerificationRegistry verificationRegistry; + @Inject + public VerificationMethodSuggestions(@NotNull VerificationRegistry verificationRegistry) { + this.verificationRegistry = verificationRegistry; + } + @Override public @NotNull CompletableFuture> suggestionsFuture( @NotNull CommandContext context, diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/config/defaults/commands/VerificationCommandDefinitions.java b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/config/defaults/VerificationCommandDefinitions.java similarity index 77% rename from identica-common/src/main/java/me/whereareiam/identica/common/config/defaults/commands/VerificationCommandDefinitions.java rename to identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/config/defaults/VerificationCommandDefinitions.java index 73721cd3b..35f6067d5 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/config/defaults/commands/VerificationCommandDefinitions.java +++ b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/config/defaults/VerificationCommandDefinitions.java @@ -1,19 +1,17 @@ -package me.whereareiam.identica.common.config.defaults.commands; +package me.whereareiam.identica.feature.verification.config.defaults; -import me.whereareiam.identica.common.config.defaults.commands.base.CommandDefinitions; import me.whereareiam.identica.model.CommandDefinition; import java.util.List; import java.util.Map; -public class VerificationCommandDefinitions implements CommandDefinitions { - @Override +public class VerificationCommandDefinitions { public void register(Registry registry) { registry.register( "verification", base("2fa", "Verification methods", "{alias}").toBuilder() - .hide(true) - .build() + .hide(true) + .build() ); registry.register("verification-status", base("2fa status", "Show verification status", "{alias}")); registry.register( @@ -33,8 +31,8 @@ public void register(Registry registry) { "{alias} ", Map.of("input", "Verification code") ).toBuilder() - .hide(true) - .build() + .hide(true) + .build() ); registry.register( "verification-enroll-confirm", @@ -44,8 +42,8 @@ public void register(Registry registry) { "{alias} ", Map.of("input", "Code or saved") ).toBuilder() - .hide(true) - .build() + .hide(true) + .build() ); registry.register( "verification-use", @@ -68,8 +66,17 @@ public void register(Registry registry) { registry.register( "verification-enroll-cancel", base("2fa enroll cancel", "Cancel pending verification enrollment", "{alias}").toBuilder() - .hide(true) - .build() + .hide(true) + .build() + ); + registry.register( + "admin-verification-reset", + withArguments( + "identica admin 2fa reset", + "Reset verification state for a player", + "{alias} [provider]", + Map.of("target", "Player/UUID", "provider", "Provider id") + ) ); } @@ -93,4 +100,9 @@ private CommandDefinition withArguments( .arguments(arguments) .build(); } + + @FunctionalInterface + public interface Registry { + void register(String key, CommandDefinition definition); + } } diff --git a/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/config/defaults/VerificationCommandsDefaults.java b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/config/defaults/VerificationCommandsDefaults.java new file mode 100644 index 000000000..09f937c54 --- /dev/null +++ b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/config/defaults/VerificationCommandsDefaults.java @@ -0,0 +1,14 @@ +package me.whereareiam.identica.feature.verification.config.defaults; + +import com.google.inject.Singleton; +import me.whereareiam.configura.merge.defaults.DefaultsProvider; +import me.whereareiam.identica.feature.verification.config.VerificationCommands; + +@Singleton +public class VerificationCommandsDefaults implements DefaultsProvider { + @Override + public VerificationCommands supply(VerificationCommands commands) { + new VerificationCommandDefinitions().register(commands.getCommands()::put); + return commands; + } +} diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/config/defaults/VerificationDefaults.java b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/config/defaults/VerificationDefaults.java similarity index 67% rename from identica-common/src/main/java/me/whereareiam/identica/common/config/defaults/VerificationDefaults.java rename to identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/config/defaults/VerificationDefaults.java index 6c11037de..762be839e 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/config/defaults/VerificationDefaults.java +++ b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/config/defaults/VerificationDefaults.java @@ -1,20 +1,20 @@ -package me.whereareiam.identica.common.config.defaults; +package me.whereareiam.identica.feature.verification.config.defaults; import com.google.inject.Singleton; import me.whereareiam.configura.merge.defaults.DefaultsProvider; -import me.whereareiam.identica.model.config.Verification; +import me.whereareiam.identica.feature.verification.model.config.VerificationSettings; import java.time.Duration; @Singleton -public class VerificationDefaults implements DefaultsProvider { +public class VerificationDefaults implements DefaultsProvider { @Override - public Verification supply(Verification verification) { + public VerificationSettings supply(VerificationSettings verification) { verification.setChallengeTtl(Duration.ofMinutes(2)); verification.setEnrollmentTtl(Duration.ofMinutes(10)); verification.setAutoSelectCurrentProvider(true); - Verification.Totp totp = new Verification.Totp(); + VerificationSettings.Totp totp = new VerificationSettings.Totp(); totp.setDisplayName("Authenticator App"); totp.setIssuer("Identica"); totp.setLabelFormat("{player}@{providerId}"); @@ -23,7 +23,7 @@ public Verification supply(Verification verification) { totp.setAllowedPastWindows(1); totp.setAllowedFutureWindows(1); - Verification.RecoveryCodes recoveryCodes = new Verification.RecoveryCodes(); + VerificationSettings.RecoveryCodes recoveryCodes = new VerificationSettings.RecoveryCodes(); recoveryCodes.setEnabled(true); recoveryCodes.setAmount(8); recoveryCodes.setLength(10); diff --git a/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/config/defaults/VerificationMessagesDefaults.java b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/config/defaults/VerificationMessagesDefaults.java new file mode 100644 index 000000000..d748fcd2d --- /dev/null +++ b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/config/defaults/VerificationMessagesDefaults.java @@ -0,0 +1,136 @@ +package me.whereareiam.identica.feature.verification.config.defaults; + +import com.google.inject.Singleton; +import me.whereareiam.configura.merge.defaults.DefaultsProvider; +import me.whereareiam.identica.feature.verification.config.VerificationMessages; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +@Singleton +public class VerificationMessagesDefaults implements DefaultsProvider { + @Override + public VerificationMessages supply(@NotNull VerificationMessages messages) { + VerificationMessages.Commands commands = new VerificationMessages.Commands(); + VerificationMessages.Commands.Admin admin = new VerificationMessages.Commands.Admin(); + VerificationMessages.Commands.Admin.Reset reset = new VerificationMessages.Commands.Admin.Reset(); + reset.setTargetNotFound("{prefix}No account found for {target}."); + reset.setCompleted("{prefix}Verification state reset for {target}."); + admin.setReset(reset); + commands.setAdmin(admin); + + commands.setPlayerOnly("{prefix}This command can only be used by a player."); + commands.setNotAllowed("{prefix}This verification action is not allowed."); + + VerificationMessages.Commands.Status status = new VerificationMessages.Commands.Status(); + status.setBody(List.of( + " ", + " Identica", + " Here you can see all enrolled 2FA methods", + " and to which providers they are assigned.", + " ", + " Enrolled methods:", + "{enrollments}", + " ", + " Provider selections:", + "{selections}", + " " + )); + status.setEmptyEnrollments(" ▪ No enrolled methods"); + status.setEmptySelections(" ▪ No provider selections"); + + VerificationMessages.Commands.Enroll enroll = new VerificationMessages.Commands.Enroll(); + enroll.setUnknownMethod("{prefix}Unknown verification method {methodDisplayName}."); + enroll.setAlreadyEnrolled("{prefix}Method {methodDisplayName} is already enrolled."); + + VerificationMessages.Commands.Confirm confirm = new VerificationMessages.Commands.Confirm(); + confirm.setNoPending("{prefix}No pending verification action."); + confirm.setInvalidCode("{prefix}Invalid verification code."); + confirm.setProtectedActionSelectionRequired("{prefix}Select a verification method for your current provider before continuing."); + confirm.setProtectedActionSessionRequired("{prefix}You must have an active session to confirm this protected action."); + confirm.setMethodUnavailable("{prefix}Method {methodDisplayName} is not available for {provider}."); + confirm.setEnabled("{prefix}Verification method {methodDisplayName} enabled."); + confirm.setAutoSelected("{prefix}Verification method {methodDisplayName} was automatically selected for {provider}."); + VerificationMessages.Commands.Confirm.RecoveryCodes recoveryCodes = + new VerificationMessages.Commands.Confirm.RecoveryCodes(); + recoveryCodes.setLayout(VerificationMessages.Commands.Confirm.RecoveryCodes.Layout.TWO_COLUMN); + recoveryCodes.setBody(List.of( + " ", + " Identica", + " Use these recovery codes if you lose access", + " to your authenticator application.", + " ", + " Store them in a safe place before continuing:", + "{entries}", + " ", + " [CONFIRM] " + + "[CANCEL]", + " " + )); + VerificationMessages.Commands.EntryFormat recoveryEntry = new VerificationMessages.Commands.EntryFormat(); + recoveryEntry.setFormat(" {code}"); + recoveryEntry.setEmptyFormat(" {code}"); + recoveryCodes.setSingleColumnEntry(recoveryEntry); + VerificationMessages.Commands.EntryFormat twoColumnRecoveryEntry = new VerificationMessages.Commands.EntryFormat(); + twoColumnRecoveryEntry.setFormat(" {left} {right}"); + twoColumnRecoveryEntry.setEmptyFormat(" {left}"); + recoveryCodes.setTwoColumnEntry(twoColumnRecoveryEntry); + recoveryCodes.setEmpty(" No recovery codes generated"); + confirm.setRecoveryCodes(recoveryCodes); + + VerificationMessages.Commands.EntryFormat enrollmentEntry = new VerificationMessages.Commands.EntryFormat(); + enrollmentEntry.setFormat(" ▪ {methodDisplayName} [{enabledAt}]"); + enrollmentEntry.setEmptyFormat(" ▪ {methodDisplayName}"); + status.setEnrollmentEntry(enrollmentEntry); + + VerificationMessages.Commands.EntryFormat selectionEntry = new VerificationMessages.Commands.EntryFormat(); + selectionEntry.setFormat(" ▪ {provider}: {methodDisplayName}"); + selectionEntry.setEmptyFormat(" ▪ {provider}"); + status.setSelectionEntry(selectionEntry); + + VerificationMessages.Commands.Use use = new VerificationMessages.Commands.Use(); + use.setProviderNotFound("{prefix}Unknown provider {provider}."); + use.setProviderUnsupported("{prefix}Provider {provider} does not support verification."); + use.setProviderVerificationDisabled("{prefix}Verification is disabled for {provider}."); + use.setMethodNotEnrolled("{prefix}Method {methodDisplayName} is not enrolled."); + use.setMethodDisabledForProvider("{prefix}Method {methodDisplayName} is not enabled for {provider}."); + use.setAlreadySelected("{prefix}{methodDisplayName} is already selected for {provider}."); + use.setUpdated("{prefix}{methodDisplayName} selected for {provider}."); + + VerificationMessages.Commands.Disable disable = new VerificationMessages.Commands.Disable(); + disable.setMethodNotEnrolled("{prefix}Method {methodDisplayName} is not enrolled."); + disable.setProtectedPrompt("{prefix}Confirm the code from your authenticator with /2fa confirm [Code] to disable {methodDisplayName}."); + disable.setDisabled("{prefix}Verification method {methodDisplayName} disabled."); + + VerificationMessages.Commands.Cancel cancel = new VerificationMessages.Commands.Cancel(); + cancel.setNoPending("{prefix}No pending verification action."); + cancel.setCancelled("{prefix}Pending verification enrollment cancelled."); + cancel.setCancelledProtectedAction("{prefix}Pending protected action cancelled."); + + VerificationMessages.Methods.Totp totp = new VerificationMessages.Methods.Totp(); + totp.setPending(List.of( + " ", + " Identica", + " ", + " Scan or enter this TOTP secret:", + " {secret}", + " ", + " [OPEN QR CODE]", + " ", + " Use /2fa enroll confirm [Code] to continue.", + " " + )); + VerificationMessages.Methods methods = new VerificationMessages.Methods(); + methods.setTotp(totp); + + messages.setMethods(methods); + commands.setStatus(status); + commands.setEnroll(enroll); + commands.setConfirm(confirm); + commands.setUse(use); + commands.setDisable(disable); + commands.setCancel(cancel); + messages.setCommands(commands); + return messages; + } +} diff --git a/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/config/defaults/VerificationProvidersDefaults.java b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/config/defaults/VerificationProvidersDefaults.java new file mode 100644 index 000000000..8a11bf59d --- /dev/null +++ b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/config/defaults/VerificationProvidersDefaults.java @@ -0,0 +1,42 @@ +package me.whereareiam.identica.feature.verification.config.defaults; + +import com.google.inject.Singleton; +import me.whereareiam.configura.merge.defaults.DefaultsProvider; +import me.whereareiam.identica.feature.verification.model.config.VerificationProviders; +import me.whereareiam.identica.feature.verification.type.UnavailableSelectionPolicy; +import me.whereareiam.identica.model.config.provider.Providers; + +import java.util.List; + +@Singleton +public class VerificationProvidersDefaults implements DefaultsProvider { + @Override + public Providers supply(Providers config) { + for (Providers.ProviderEntry entry : config.getProviders()) { + if (entry == null || entry.getId().isBlank()) continue; + if (!(entry.getFeatures() instanceof VerificationProviders features)) continue; + + if ("credential".equalsIgnoreCase(entry.getId())) + features.setVerification(defaultVerification()); + if ("premium".equalsIgnoreCase(entry.getId())) + features.setVerification(defaultVerification()); + } + + return config; + } + + private VerificationProviders.Verification defaultVerification() { + VerificationProviders.Verification verification = new VerificationProviders.Verification(); + verification.setEnabled(true); + verification.setRequired(false); + verification.setUnavailableSelectionPolicy(UnavailableSelectionPolicy.KEEP_LOCKED); + + VerificationProviders.Verification.MethodEntry totp = new VerificationProviders.Verification.MethodEntry(); + totp.setId("totp"); + totp.setEnabled(true); + totp.setPriority(100); + totp.setUnavailableSelectionPolicy(UnavailableSelectionPolicy.KEEP_LOCKED); + verification.setMethods(List.of(totp)); + return verification; + } +} diff --git a/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/config/provider/VerificationCommandsProvider.java b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/config/provider/VerificationCommandsProvider.java new file mode 100644 index 000000000..5f3f30b58 --- /dev/null +++ b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/config/provider/VerificationCommandsProvider.java @@ -0,0 +1,30 @@ +package me.whereareiam.identica.feature.verification.config.provider; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.google.inject.name.Named; +import me.whereareiam.configura.Config; +import me.whereareiam.configura.Configura; +import me.whereareiam.identica.Registry; +import me.whereareiam.identica.Reloadable; +import me.whereareiam.identica.config.ConfigProvider; +import me.whereareiam.identica.feature.verification.config.VerificationCommands; +import me.whereareiam.identica.feature.verification.config.defaults.VerificationCommandsDefaults; + +import java.nio.file.Path; + +@Singleton +public class VerificationCommandsProvider extends ConfigProvider { + @Inject + public VerificationCommandsProvider( + @Named("verificationFeaturePath") Path verificationFeaturePath, + Registry registry + ) { + super(verificationFeaturePath, "commands", VerificationCommands.class, registry); + } + + @Override + protected Configura configura() { + return versioned(Config.configured().withDefaults(VerificationCommandsDefaults.class), VerificationCommands.class); + } +} diff --git a/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/config/provider/VerificationMessagesProvider.java b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/config/provider/VerificationMessagesProvider.java new file mode 100644 index 000000000..fc714ecab --- /dev/null +++ b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/config/provider/VerificationMessagesProvider.java @@ -0,0 +1,30 @@ +package me.whereareiam.identica.feature.verification.config.provider; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.google.inject.name.Named; +import me.whereareiam.configura.Config; +import me.whereareiam.configura.Configura; +import me.whereareiam.identica.Registry; +import me.whereareiam.identica.Reloadable; +import me.whereareiam.identica.config.ConfigProvider; +import me.whereareiam.identica.feature.verification.config.VerificationMessages; +import me.whereareiam.identica.feature.verification.config.defaults.VerificationMessagesDefaults; + +import java.nio.file.Path; + +@Singleton +public class VerificationMessagesProvider extends ConfigProvider { + @Inject + public VerificationMessagesProvider( + @Named("verificationFeaturePath") Path verificationFeaturePath, + Registry registry + ) { + super(verificationFeaturePath, "messages", VerificationMessages.class, registry); + } + + @Override + protected Configura configura() { + return versioned(Config.configured().withDefaults(VerificationMessagesDefaults.class), VerificationMessages.class); + } +} diff --git a/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/config/provider/VerificationProvidersProvider.java b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/config/provider/VerificationProvidersProvider.java new file mode 100644 index 000000000..2c4bd11bb --- /dev/null +++ b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/config/provider/VerificationProvidersProvider.java @@ -0,0 +1,49 @@ +package me.whereareiam.identica.feature.verification.config.provider; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.google.inject.name.Named; +import me.whereareiam.configura.Config; +import me.whereareiam.configura.Configura; +import me.whereareiam.configura.document.DocumentTypeContext; +import me.whereareiam.configura.feature.extension.ExtensionFeature; +import me.whereareiam.configura.feature.extension.api.ConfigDocumentRule; +import me.whereareiam.identica.Registry; +import me.whereareiam.identica.Reloadable; +import me.whereareiam.identica.config.ConfigProvider; +import me.whereareiam.identica.feature.verification.config.defaults.VerificationProvidersDefaults; +import me.whereareiam.identica.feature.verification.model.config.VerificationProviders; +import me.whereareiam.identica.model.config.provider.Providers; +import org.jetbrains.annotations.NotNull; + +import java.nio.file.Path; + +@Singleton +public class VerificationProvidersProvider extends ConfigProvider { + @Inject + public VerificationProvidersProvider( + @Named("providersPath") Path providersPath, + Registry reloadables + ) { + super(providersPath, "providers", Providers.class, reloadables); + } + + @Override + protected Configura configura() { + return versioned(Config.configured() + .withDefaults(VerificationProvidersDefaults.class) + .withFeature(ExtensionFeature.rule( + ConfigDocumentRule.when( + Providers.ProviderEntry.Features.class, + VerificationProviders.class, + VerificationProvidersProvider::isVerificationFeaturesNode + ) + )), Providers.class); + } + + private static boolean isVerificationFeaturesNode(@NotNull DocumentTypeContext context) { + return "features".equals(context.getFieldName()) + && context.getCurrentNode() != null + && context.getCurrentNode().has("verification"); + } +} diff --git a/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/config/provider/VerificationSettingsProvider.java b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/config/provider/VerificationSettingsProvider.java new file mode 100644 index 000000000..675c72637 --- /dev/null +++ b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/config/provider/VerificationSettingsProvider.java @@ -0,0 +1,30 @@ +package me.whereareiam.identica.feature.verification.config.provider; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.google.inject.name.Named; +import me.whereareiam.configura.Config; +import me.whereareiam.configura.Configura; +import me.whereareiam.identica.Registry; +import me.whereareiam.identica.Reloadable; +import me.whereareiam.identica.config.ConfigProvider; +import me.whereareiam.identica.feature.verification.config.defaults.VerificationDefaults; +import me.whereareiam.identica.feature.verification.model.config.VerificationSettings; + +import java.nio.file.Path; + +@Singleton +public class VerificationSettingsProvider extends ConfigProvider { + @Inject + public VerificationSettingsProvider( + @Named("verificationFeaturePath") Path verificationFeaturePath, + Registry registry + ) { + super(verificationFeaturePath, "settings", VerificationSettings.class, registry); + } + + @Override + protected Configura configura() { + return versioned(Config.configured().withDefaults(VerificationDefaults.class), VerificationSettings.class); + } +} diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/verification/enrollment/VerificationEnrollmentActivator.java b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/enrollment/VerificationEnrollmentActivator.java similarity index 69% rename from identica-common/src/main/java/me/whereareiam/identica/common/verification/enrollment/VerificationEnrollmentActivator.java rename to identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/enrollment/VerificationEnrollmentActivator.java index e5c457a5e..28e162a14 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/verification/enrollment/VerificationEnrollmentActivator.java +++ b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/enrollment/VerificationEnrollmentActivator.java @@ -1,17 +1,17 @@ -package me.whereareiam.identica.common.verification.enrollment; +package me.whereareiam.identica.feature.verification.enrollment; import com.google.inject.Inject; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.common.verification.RecoveryCodeGenerator; -import me.whereareiam.identica.database.VerificationPersistenceService; import me.whereareiam.identica.event.EventManager; -import me.whereareiam.identica.event.verification.enroll.VerificationEnrollmentConfirmedEvent; -import me.whereareiam.identica.model.verification.VerificationRecoveryCode; -import me.whereareiam.identica.model.verification.enrollment.PendingVerificationEnrollment; -import me.whereareiam.identica.model.verification.enrollment.VerificationEnrollment; -import me.whereareiam.identica.model.verification.enrollment.VerificationEnrollmentResult; -import me.whereareiam.identica.model.verification.enrollment.VerificationEnrollmentState; +import me.whereareiam.identica.feature.verification.database.VerificationPersistenceService; +import me.whereareiam.identica.feature.verification.event.enroll.VerificationEnrollmentConfirmedEvent; +import me.whereareiam.identica.feature.verification.model.VerificationRecoveryCode; +import me.whereareiam.identica.feature.verification.model.enrollment.PendingVerificationEnrollment; +import me.whereareiam.identica.feature.verification.model.enrollment.VerificationEnrollment; +import me.whereareiam.identica.feature.verification.model.enrollment.VerificationEnrollmentResult; +import me.whereareiam.identica.feature.verification.state.VerificationEnrollmentState; +import me.whereareiam.identica.feature.verification.type.totp.RecoveryCodeGenerator; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/verification/enrollment/VerificationEnrollmentLifecycle.java b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/enrollment/VerificationEnrollmentLifecycle.java similarity index 83% rename from identica-common/src/main/java/me/whereareiam/identica/common/verification/enrollment/VerificationEnrollmentLifecycle.java rename to identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/enrollment/VerificationEnrollmentLifecycle.java index 382a5809b..e3bc574d1 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/verification/enrollment/VerificationEnrollmentLifecycle.java +++ b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/enrollment/VerificationEnrollmentLifecycle.java @@ -1,24 +1,24 @@ -package me.whereareiam.identica.common.verification.enrollment; +package me.whereareiam.identica.feature.verification.enrollment; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.common.verification.codec.VerificationStateCodec; -import me.whereareiam.identica.database.VerificationPersistenceService; import me.whereareiam.identica.event.EventManager; -import me.whereareiam.identica.event.verification.enroll.VerificationEnrollEvent; -import me.whereareiam.identica.model.config.Verification; -import me.whereareiam.identica.model.verification.enrollment.PendingVerificationEnrollment; -import me.whereareiam.identica.model.verification.enrollment.VerificationEnrollmentContext; -import me.whereareiam.identica.model.verification.enrollment.VerificationEnrollmentResult; -import me.whereareiam.identica.model.verification.enrollment.VerificationEnrollmentState; -import me.whereareiam.identica.type.verification.VerificationEnrollmentStatus; -import me.whereareiam.identica.type.verification.VerificationMethodCapability; -import me.whereareiam.identica.verification.VerificationInteraction; -import me.whereareiam.identica.verification.VerificationMethod; -import me.whereareiam.identica.verification.VerificationRegistry; -import me.whereareiam.identica.verification.process.VerificationEnrollmentProcess; +import me.whereareiam.identica.feature.verification.VerificationInteraction; +import me.whereareiam.identica.feature.verification.VerificationMethod; +import me.whereareiam.identica.feature.verification.VerificationRegistry; +import me.whereareiam.identica.feature.verification.challenge.VerificationStateCodec; +import me.whereareiam.identica.feature.verification.database.VerificationPersistenceService; +import me.whereareiam.identica.feature.verification.event.enroll.VerificationEnrollEvent; +import me.whereareiam.identica.feature.verification.model.config.VerificationSettings; +import me.whereareiam.identica.feature.verification.model.enrollment.PendingVerificationEnrollment; +import me.whereareiam.identica.feature.verification.model.enrollment.VerificationEnrollmentContext; +import me.whereareiam.identica.feature.verification.model.enrollment.VerificationEnrollmentResult; +import me.whereareiam.identica.feature.verification.process.VerificationEnrollmentProcess; +import me.whereareiam.identica.feature.verification.state.VerificationEnrollmentState; +import me.whereareiam.identica.feature.verification.type.VerificationMethodCapability; +import me.whereareiam.identica.feature.verification.type.status.VerificationEnrollmentStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -31,7 +31,7 @@ public class VerificationEnrollmentLifecycle { private final VerificationEnrollmentStore enrollmentStore; private final VerificationStateCodec stateCodec; private final VerificationRegistry methodRegistry; - private final Provider verificationProvider; + private final Provider verificationProvider; private final EventManager eventManager; private final VerificationEnrollmentActivator enrollmentActivator; diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/verification/enrollment/VerificationEnrollmentStore.java b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/enrollment/VerificationEnrollmentStore.java similarity index 77% rename from identica-common/src/main/java/me/whereareiam/identica/common/verification/enrollment/VerificationEnrollmentStore.java rename to identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/enrollment/VerificationEnrollmentStore.java index 0cfa5bbc4..fcb53aa5e 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/verification/enrollment/VerificationEnrollmentStore.java +++ b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/enrollment/VerificationEnrollmentStore.java @@ -1,10 +1,10 @@ -package me.whereareiam.identica.common.verification.enrollment; +package me.whereareiam.identica.feature.verification.enrollment; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; -import me.whereareiam.identica.model.config.Verification; -import me.whereareiam.identica.model.verification.enrollment.PendingVerificationEnrollment; +import me.whereareiam.identica.feature.verification.model.config.VerificationSettings; +import me.whereareiam.identica.feature.verification.model.enrollment.PendingVerificationEnrollment; import me.whereareiam.identica.replication.ReplicationSystem; import me.whereareiam.identica.replication.cache.LocalCache; import org.jetbrains.annotations.NotNull; @@ -17,12 +17,12 @@ public class VerificationEnrollmentStore { private static final String NAMESPACE = "verification:enrollment"; private final LocalCache cache; - private final Provider verificationProvider; + private final Provider verificationProvider; @Inject public VerificationEnrollmentStore( @NotNull ReplicationSystem replicationSystem, - @NotNull Provider verificationProvider + @NotNull Provider verificationProvider ) { this.cache = replicationSystem.cache(NAMESPACE).local(); this.verificationProvider = verificationProvider; diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/verification/resolution/VerificationRequirementResolver.java b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/resolution/VerificationRequirementResolver.java similarity index 77% rename from identica-common/src/main/java/me/whereareiam/identica/common/verification/resolution/VerificationRequirementResolver.java rename to identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/resolution/VerificationRequirementResolver.java index 149701de0..3a20ff106 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/verification/resolution/VerificationRequirementResolver.java +++ b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/resolution/VerificationRequirementResolver.java @@ -1,25 +1,25 @@ -package me.whereareiam.identica.common.verification.resolution; +package me.whereareiam.identica.feature.verification.resolution; import com.google.inject.Inject; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.common.verification.VerificationPolicyResolver; -import me.whereareiam.identica.common.verification.challenge.VerificationChallengeLifecycle; -import me.whereareiam.identica.common.verification.challenge.VerificationChallengeStore; -import me.whereareiam.identica.database.VerificationPersistenceService; -import me.whereareiam.identica.model.verification.VerificationResolutionRequest; -import me.whereareiam.identica.model.verification.VerificationResolutionResult; -import me.whereareiam.identica.model.verification.challenge.PendingVerificationChallenge; -import me.whereareiam.identica.model.verification.challenge.VerificationChallengeResult; -import me.whereareiam.identica.model.verification.enrollment.VerificationEnrollment; -import me.whereareiam.identica.model.verification.selection.VerificationSelection; +import me.whereareiam.identica.feature.verification.VerificationMethod; +import me.whereareiam.identica.feature.verification.VerificationPolicyResolver; +import me.whereareiam.identica.feature.verification.VerificationRegistry; +import me.whereareiam.identica.feature.verification.challenge.VerificationChallengeLifecycle; +import me.whereareiam.identica.feature.verification.challenge.VerificationChallengeStore; +import me.whereareiam.identica.feature.verification.database.VerificationPersistenceService; +import me.whereareiam.identica.feature.verification.model.challenge.PendingVerificationChallenge; +import me.whereareiam.identica.feature.verification.model.challenge.VerificationChallengeResult; +import me.whereareiam.identica.feature.verification.model.enrollment.VerificationEnrollment; +import me.whereareiam.identica.feature.verification.model.resolution.VerificationResolutionRequest; +import me.whereareiam.identica.feature.verification.model.resolution.VerificationResolutionResult; +import me.whereareiam.identica.feature.verification.model.selection.VerificationSelection; +import me.whereareiam.identica.feature.verification.type.UnavailableSelectionPolicy; +import me.whereareiam.identica.feature.verification.type.status.VerificationChallengeStatus; +import me.whereareiam.identica.feature.verification.type.status.VerificationResolutionStatus; import me.whereareiam.identica.provider.ProviderManager; -import me.whereareiam.identica.type.provider.ProviderCapability; -import me.whereareiam.identica.type.verification.UnavailableSelectionPolicy; -import me.whereareiam.identica.type.verification.VerificationChallengeStatus; -import me.whereareiam.identica.type.verification.VerificationResolutionStatus; -import me.whereareiam.identica.verification.VerificationMethod; -import me.whereareiam.identica.verification.VerificationRegistry; +import me.whereareiam.identica.type.provider.capability.ProviderCapability; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -29,6 +29,8 @@ @Singleton @RequiredArgsConstructor(onConstructor_ = @Inject) public class VerificationRequirementResolver { + // TODO Migrate and use capbility api class + private static final ProviderCapability VERIFICATION_CAPABILITY = ProviderCapability.of("verification"); private final VerificationPersistenceService persistenceService; private final VerificationChallengeStore challengeStore; private final VerificationPolicyResolver policyResolver; @@ -152,9 +154,11 @@ public class VerificationRequirementResolver { if (status == VerificationChallengeStatus.VERIFIED) return VerificationResolutionStatus.SATISFIED; if (status == VerificationChallengeStatus.WAITING || status == VerificationChallengeStatus.INVALID) return VerificationResolutionStatus.WAITING; + if (status == VerificationChallengeStatus.PROVIDER_UNSUPPORTED || status == VerificationChallengeStatus.PROVIDER_VERIFICATION_DISABLED) return VerificationResolutionStatus.SKIPPED; + return VerificationResolutionStatus.DENIED; } @@ -165,6 +169,6 @@ private boolean supportsVerification(@Nullable String providerId) { .anyMatch(provider -> provider != null && provider.getDescriptor() != null && providerId.equalsIgnoreCase(provider.getDescriptor().getId()) - && provider.getDescriptor().hasCapability(ProviderCapability.VERIFICATION)); + && provider.getDescriptor().hasCapability(VERIFICATION_CAPABILITY)); } } diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/verification/RecoveryCodeGenerator.java b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/type/totp/RecoveryCodeGenerator.java similarity index 96% rename from identica-common/src/main/java/me/whereareiam/identica/common/verification/RecoveryCodeGenerator.java rename to identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/type/totp/RecoveryCodeGenerator.java index 76f55d208..fd7783694 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/verification/RecoveryCodeGenerator.java +++ b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/type/totp/RecoveryCodeGenerator.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.common.verification; +package me.whereareiam.identica.feature.verification.type.totp; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/verification/type/totp/TotpCodec.java b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/type/totp/TotpCodec.java similarity index 98% rename from identica-common/src/main/java/me/whereareiam/identica/common/verification/type/totp/TotpCodec.java rename to identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/type/totp/TotpCodec.java index ba4046a97..1a8173170 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/verification/type/totp/TotpCodec.java +++ b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/type/totp/TotpCodec.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.common.verification.type.totp; +package me.whereareiam.identica.feature.verification.type.totp; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/verification/type/totp/TotpVerificationMethod.java b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/type/totp/TotpVerificationMethod.java similarity index 59% rename from identica-common/src/main/java/me/whereareiam/identica/common/verification/type/totp/TotpVerificationMethod.java rename to identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/type/totp/TotpVerificationMethod.java index b629d06e1..459992ca8 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/verification/type/totp/TotpVerificationMethod.java +++ b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/type/totp/TotpVerificationMethod.java @@ -1,17 +1,17 @@ -package me.whereareiam.identica.common.verification.type.totp; +package me.whereareiam.identica.feature.verification.type.totp; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.model.config.Verification; -import me.whereareiam.identica.model.verification.VerificationMethodDescriptor; -import me.whereareiam.identica.type.verification.VerificationMethodCapability; -import me.whereareiam.identica.verification.process.VerificationChallengeProcess; -import me.whereareiam.identica.verification.process.VerificationEnrollmentProcess; -import me.whereareiam.identica.verification.VerificationMethod; -import me.whereareiam.identica.common.verification.type.totp.process.TotpChallengeProcess; -import me.whereareiam.identica.common.verification.type.totp.process.TotpEnrollmentProcess; +import me.whereareiam.identica.feature.verification.VerificationMethod; +import me.whereareiam.identica.feature.verification.model.VerificationMethodDescriptor; +import me.whereareiam.identica.feature.verification.model.config.VerificationSettings; +import me.whereareiam.identica.feature.verification.process.VerificationChallengeProcess; +import me.whereareiam.identica.feature.verification.process.VerificationEnrollmentProcess; +import me.whereareiam.identica.feature.verification.type.VerificationMethodCapability; +import me.whereareiam.identica.feature.verification.type.totp.process.TotpChallengeProcess; +import me.whereareiam.identica.feature.verification.type.totp.process.TotpEnrollmentProcess; import org.jetbrains.annotations.NotNull; import java.util.Set; @@ -19,7 +19,7 @@ @Singleton @RequiredArgsConstructor(onConstructor_ = @Inject) public class TotpVerificationMethod implements VerificationMethod { - private final Provider verificationProvider; + private final Provider verificationProvider; private final TotpEnrollmentProcess enrollmentProcess; private final TotpChallengeProcess challengeProcess; diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/verification/type/totp/process/TotpChallengeProcess.java b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/type/totp/process/TotpChallengeProcess.java similarity index 73% rename from identica-common/src/main/java/me/whereareiam/identica/common/verification/type/totp/process/TotpChallengeProcess.java rename to identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/type/totp/process/TotpChallengeProcess.java index f0722fe3e..83ea25638 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/verification/type/totp/process/TotpChallengeProcess.java +++ b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/type/totp/process/TotpChallengeProcess.java @@ -1,17 +1,17 @@ -package me.whereareiam.identica.common.verification.type.totp.process; +package me.whereareiam.identica.feature.verification.type.totp.process; import com.google.inject.Inject; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; -import me.whereareiam.identica.model.verification.challenge.VerificationChallengeContext; -import me.whereareiam.identica.model.verification.challenge.VerificationChallengeResult; -import me.whereareiam.identica.model.verification.process.VerificationProcessContext; -import me.whereareiam.identica.model.verification.process.VerificationProcessResult; -import me.whereareiam.identica.type.verification.VerificationChallengeStatus; -import me.whereareiam.identica.verification.process.VerificationChallengeProcess; -import me.whereareiam.identica.verification.VerificationInteraction; -import me.whereareiam.identica.common.verification.type.totp.state.TotpChallengeState; -import me.whereareiam.identica.common.verification.type.totp.step.TotpChallengeVerificationStep; +import me.whereareiam.identica.feature.verification.VerificationInteraction; +import me.whereareiam.identica.feature.verification.model.challenge.VerificationChallengeContext; +import me.whereareiam.identica.feature.verification.model.challenge.VerificationChallengeResult; +import me.whereareiam.identica.feature.verification.model.process.VerificationProcessContext; +import me.whereareiam.identica.feature.verification.model.process.VerificationProcessResult; +import me.whereareiam.identica.feature.verification.process.VerificationChallengeProcess; +import me.whereareiam.identica.feature.verification.type.status.VerificationChallengeStatus; +import me.whereareiam.identica.feature.verification.type.totp.state.TotpChallengeState; +import me.whereareiam.identica.feature.verification.type.totp.step.TotpChallengeVerificationStep; import org.jetbrains.annotations.NotNull; @Singleton diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/verification/type/totp/process/TotpEnrollmentProcess.java b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/type/totp/process/TotpEnrollmentProcess.java similarity index 73% rename from identica-common/src/main/java/me/whereareiam/identica/common/verification/type/totp/process/TotpEnrollmentProcess.java rename to identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/type/totp/process/TotpEnrollmentProcess.java index 1ea574daa..c2daeefaa 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/verification/type/totp/process/TotpEnrollmentProcess.java +++ b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/type/totp/process/TotpEnrollmentProcess.java @@ -1,21 +1,21 @@ -package me.whereareiam.identica.common.verification.type.totp.process; +package me.whereareiam.identica.feature.verification.type.totp.process; import com.google.inject.Inject; import com.google.inject.Singleton; import lombok.Getter; -import me.whereareiam.identica.model.verification.enrollment.VerificationEnrollmentContext; -import me.whereareiam.identica.model.verification.enrollment.VerificationEnrollmentResult; -import me.whereareiam.identica.model.verification.process.VerificationProcessContext; -import me.whereareiam.identica.model.verification.process.VerificationProcessResult; -import me.whereareiam.identica.verification.process.base.VerificationProcessOrchestrator; -import me.whereareiam.identica.type.verification.VerificationEnrollmentStatus; -import me.whereareiam.identica.verification.process.VerificationEnrollmentProcess; -import me.whereareiam.identica.verification.VerificationInteraction; -import me.whereareiam.identica.verification.process.VerificationProcessStep; -import me.whereareiam.identica.common.verification.type.totp.state.TotpEnrollmentState; -import me.whereareiam.identica.common.verification.type.totp.step.TotpConfirmCodeStep; -import me.whereareiam.identica.common.verification.type.totp.step.TotpConfirmSavedStep; -import me.whereareiam.identica.common.verification.type.totp.step.TotpSetupStep; +import me.whereareiam.identica.feature.verification.VerificationInteraction; +import me.whereareiam.identica.feature.verification.model.enrollment.VerificationEnrollmentContext; +import me.whereareiam.identica.feature.verification.model.enrollment.VerificationEnrollmentResult; +import me.whereareiam.identica.feature.verification.model.process.VerificationProcessContext; +import me.whereareiam.identica.feature.verification.model.process.VerificationProcessResult; +import me.whereareiam.identica.feature.verification.process.VerificationEnrollmentProcess; +import me.whereareiam.identica.feature.verification.process.VerificationProcessStep; +import me.whereareiam.identica.feature.verification.process.base.VerificationProcessOrchestrator; +import me.whereareiam.identica.feature.verification.type.status.VerificationEnrollmentStatus; +import me.whereareiam.identica.feature.verification.type.totp.state.TotpEnrollmentState; +import me.whereareiam.identica.feature.verification.type.totp.step.TotpConfirmCodeStep; +import me.whereareiam.identica.feature.verification.type.totp.step.TotpConfirmSavedStep; +import me.whereareiam.identica.feature.verification.type.totp.step.TotpSetupStep; import org.jetbrains.annotations.NotNull; import java.util.List; diff --git a/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/type/totp/state/TotpChallengeState.java b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/type/totp/state/TotpChallengeState.java new file mode 100644 index 000000000..f398e556b --- /dev/null +++ b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/type/totp/state/TotpChallengeState.java @@ -0,0 +1,16 @@ +package me.whereareiam.identica.feature.verification.type.totp.state; + +import lombok.*; +import me.whereareiam.identica.feature.verification.state.VerificationChallengeState; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Builder(toBuilder = true) +public class TotpChallengeState implements VerificationChallengeState { + private String stepId; + private int attempts; + private boolean recoveryCodeUsed; +} diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/verification/type/totp/state/TotpEnrollmentState.java b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/type/totp/state/TotpEnrollmentState.java similarity index 61% rename from identica-common/src/main/java/me/whereareiam/identica/common/verification/type/totp/state/TotpEnrollmentState.java rename to identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/type/totp/state/TotpEnrollmentState.java index 37afca9bd..eeddd5b57 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/verification/type/totp/state/TotpEnrollmentState.java +++ b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/type/totp/state/TotpEnrollmentState.java @@ -1,13 +1,8 @@ -package me.whereareiam.identica.common.verification.type.totp.state; +package me.whereareiam.identica.feature.verification.type.totp.state; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; -import me.whereareiam.identica.model.verification.enrollment.VerificationEnrollmentState; -import me.whereareiam.identica.verification.process.VerificationStepCursor; +import lombok.*; +import me.whereareiam.identica.feature.verification.process.VerificationStepCursor; +import me.whereareiam.identica.feature.verification.state.VerificationEnrollmentState; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/verification/type/totp/step/TotpChallengeVerificationStep.java b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/type/totp/step/TotpChallengeVerificationStep.java similarity index 75% rename from identica-common/src/main/java/me/whereareiam/identica/common/verification/type/totp/step/TotpChallengeVerificationStep.java rename to identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/type/totp/step/TotpChallengeVerificationStep.java index 76e4d1660..36cd1a6c3 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/verification/type/totp/step/TotpChallengeVerificationStep.java +++ b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/type/totp/step/TotpChallengeVerificationStep.java @@ -1,28 +1,28 @@ -package me.whereareiam.identica.common.verification.type.totp.step; +package me.whereareiam.identica.feature.verification.type.totp.step; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; +import me.whereareiam.identica.feature.verification.VerificationInteraction; +import me.whereareiam.identica.feature.verification.database.VerificationPersistenceService; +import me.whereareiam.identica.feature.verification.model.config.VerificationSettings; +import me.whereareiam.identica.feature.verification.model.interaction.CodeVerificationInteraction; +import me.whereareiam.identica.feature.verification.model.interaction.RecoveryCodeVerificationInteraction; +import me.whereareiam.identica.feature.verification.model.process.VerificationProcessContext; +import me.whereareiam.identica.feature.verification.model.process.VerificationProcessResult; +import me.whereareiam.identica.feature.verification.process.VerificationProcessStep; +import me.whereareiam.identica.feature.verification.type.process.VerificationProcessStatus; +import me.whereareiam.identica.feature.verification.type.totp.RecoveryCodeGenerator; +import me.whereareiam.identica.feature.verification.type.totp.TotpCodec; +import me.whereareiam.identica.feature.verification.type.totp.state.TotpChallengeState; import me.whereareiam.identica.logging.Logger; -import me.whereareiam.identica.common.verification.RecoveryCodeGenerator; -import me.whereareiam.identica.database.VerificationPersistenceService; -import me.whereareiam.identica.model.config.Verification; -import me.whereareiam.identica.model.verification.interaction.CodeVerificationInteraction; -import me.whereareiam.identica.model.verification.interaction.RecoveryCodeVerificationInteraction; -import me.whereareiam.identica.model.verification.process.VerificationProcessContext; -import me.whereareiam.identica.model.verification.process.VerificationProcessResult; -import me.whereareiam.identica.type.verification.VerificationProcessStatus; -import me.whereareiam.identica.verification.VerificationInteraction; -import me.whereareiam.identica.verification.process.VerificationProcessStep; -import me.whereareiam.identica.common.verification.type.totp.TotpCodec; -import me.whereareiam.identica.common.verification.type.totp.state.TotpChallengeState; import org.jetbrains.annotations.NotNull; @Singleton @RequiredArgsConstructor(onConstructor_ = @Inject) public class TotpChallengeVerificationStep implements VerificationProcessStep { - private final Provider verificationProvider; + private final Provider verificationProvider; private final VerificationPersistenceService persistenceService; @Override @@ -128,7 +128,7 @@ public class TotpChallengeVerificationStep implements VerificationProcessStep { - private final Provider verificationProvider; + private final Provider verificationProvider; @Inject - public TotpConfirmCodeStep(Provider verificationProvider) { + public TotpConfirmCodeStep(Provider verificationProvider) { this.verificationProvider = verificationProvider; } @@ -58,7 +59,7 @@ public TotpConfirmCodeStep(Provider verificationProvider) { if (!verify(state.getSecret(), interaction.getCode())) return VerificationProcessResult.builder() - .status(me.whereareiam.identica.type.verification.VerificationProcessStatus.INVALID) + .status(VerificationProcessStatus.INVALID) .state(state) .transition(VerificationProcessTransition.stay()) .build(); @@ -70,7 +71,7 @@ public TotpConfirmCodeStep(Provider verificationProvider) { } private boolean verify(@NotNull String secret, @NotNull String code) { - Verification.Totp totp = verificationProvider.get().getTotp(); + VerificationSettings.Totp totp = verificationProvider.get().getTotp(); return TotpCodec.verify( secret, code, @@ -82,7 +83,7 @@ private boolean verify(@NotNull String secret, @NotNull String code) { } private @NotNull List recoveryCodes() { - Verification.RecoveryCodes recoveryCodes = verificationProvider.get().getTotp().getRecoveryCodes(); + VerificationSettings.RecoveryCodes recoveryCodes = verificationProvider.get().getTotp().getRecoveryCodes(); if (!recoveryCodes.isEnabled()) return List.of(); return RecoveryCodeGenerator.generateCodes( recoveryCodes.getAmount(), diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/verification/type/totp/step/TotpConfirmSavedStep.java b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/type/totp/step/TotpConfirmSavedStep.java similarity index 64% rename from identica-common/src/main/java/me/whereareiam/identica/common/verification/type/totp/step/TotpConfirmSavedStep.java rename to identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/type/totp/step/TotpConfirmSavedStep.java index 2394982d8..fb4ebc640 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/verification/type/totp/step/TotpConfirmSavedStep.java +++ b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/type/totp/step/TotpConfirmSavedStep.java @@ -1,12 +1,12 @@ -package me.whereareiam.identica.common.verification.type.totp.step; +package me.whereareiam.identica.feature.verification.type.totp.step; import com.google.inject.Singleton; -import me.whereareiam.identica.model.verification.interaction.SavedVerificationInteraction; -import me.whereareiam.identica.model.verification.process.VerificationProcessContext; -import me.whereareiam.identica.model.verification.process.VerificationProcessResult; -import me.whereareiam.identica.model.verification.process.VerificationProcessTransition; -import me.whereareiam.identica.verification.process.VerificationProcessStep; -import me.whereareiam.identica.common.verification.type.totp.state.TotpEnrollmentState; +import me.whereareiam.identica.feature.verification.model.interaction.SavedVerificationInteraction; +import me.whereareiam.identica.feature.verification.model.process.VerificationProcessContext; +import me.whereareiam.identica.feature.verification.model.process.VerificationProcessResult; +import me.whereareiam.identica.feature.verification.model.process.VerificationProcessTransition; +import me.whereareiam.identica.feature.verification.process.VerificationProcessStep; +import me.whereareiam.identica.feature.verification.type.totp.state.TotpEnrollmentState; import org.jetbrains.annotations.NotNull; @Singleton diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/verification/type/totp/step/TotpSetupStep.java b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/type/totp/step/TotpSetupStep.java similarity index 62% rename from identica-common/src/main/java/me/whereareiam/identica/common/verification/type/totp/step/TotpSetupStep.java rename to identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/type/totp/step/TotpSetupStep.java index 317a8f033..468db50ea 100644 --- a/identica-common/src/main/java/me/whereareiam/identica/common/verification/type/totp/step/TotpSetupStep.java +++ b/identica-feature/feature-verification/verification-common/src/main/java/me/whereareiam/identica/feature/verification/type/totp/step/TotpSetupStep.java @@ -1,18 +1,19 @@ -package me.whereareiam.identica.common.verification.type.totp.step; +package me.whereareiam.identica.feature.verification.type.totp.step; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; -import me.whereareiam.identica.model.config.Messages; -import me.whereareiam.identica.model.config.Verification; -import me.whereareiam.identica.model.verification.interaction.CodeVerificationInteraction; -import me.whereareiam.identica.model.verification.process.VerificationProcessContext; -import me.whereareiam.identica.model.verification.process.VerificationProcessDisplay; -import me.whereareiam.identica.model.verification.process.VerificationProcessResult; -import me.whereareiam.identica.model.verification.process.VerificationProcessTransition; -import me.whereareiam.identica.verification.process.VerificationProcessStep; -import me.whereareiam.identica.common.verification.type.totp.TotpCodec; -import me.whereareiam.identica.common.verification.type.totp.state.TotpEnrollmentState; +import me.whereareiam.identica.feature.verification.config.VerificationMessages; +import me.whereareiam.identica.feature.verification.model.config.VerificationSettings; +import me.whereareiam.identica.feature.verification.model.interaction.CodeVerificationInteraction; +import me.whereareiam.identica.feature.verification.model.process.VerificationProcessContext; +import me.whereareiam.identica.feature.verification.model.process.VerificationProcessDisplay; +import me.whereareiam.identica.feature.verification.model.process.VerificationProcessResult; +import me.whereareiam.identica.feature.verification.model.process.VerificationProcessTransition; +import me.whereareiam.identica.feature.verification.process.VerificationProcessStep; +import me.whereareiam.identica.feature.verification.type.process.VerificationProcessStatus; +import me.whereareiam.identica.feature.verification.type.totp.TotpCodec; +import me.whereareiam.identica.feature.verification.type.totp.state.TotpEnrollmentState; import org.jetbrains.annotations.NotNull; import java.net.URLEncoder; @@ -24,11 +25,11 @@ public class TotpSetupStep implements VerificationProcessStep { private static final int SECRET_BYTES = 20; - private final Provider verificationProvider; - private final Provider messagesProvider; + private final Provider verificationProvider; + private final Provider messagesProvider; @Inject - public TotpSetupStep(Provider verificationProvider, Provider messagesProvider) { + public TotpSetupStep(Provider verificationProvider, Provider messagesProvider) { this.verificationProvider = verificationProvider; this.messagesProvider = messagesProvider; } @@ -52,7 +53,7 @@ public TotpSetupStep(Provider verificationProvider, Provider start( @NotNull VerificationProcessContext context ) { - Verification.Totp totp = verificationProvider.get().getTotp(); + VerificationSettings.Totp totp = verificationProvider.get().getTotp(); String providerId = context.getProviderId() == null || context.getProviderId().isBlank() ? "unknown" : context.getProviderId(); @@ -85,10 +86,10 @@ public TotpSetupStep(Provider verificationProvider, Providerbuilder() - .status(me.whereareiam.identica.type.verification.VerificationProcessStatus.WAITING) + .status(VerificationProcessStatus.WAITING) .state(state) .display(VerificationProcessDisplay.builder() - .lines(messagesProvider.get().getCommands().getVerification().getMethods().getTotp().getPending()) + .lines(messagesProvider.get().getMethods().getTotp().getPending()) .placeholders(placeholders) .build()) .transition(VerificationProcessTransition.advance()) diff --git a/identica-common/src/test/java/me/whereareiam/identica/common/verification/DefaultVerificationServiceEnrollmentFlowTest.java b/identica-feature/feature-verification/verification-common/src/test/java/me/whereareiam/identica/feature/verification/DefaultVerificationServiceEnrollmentFlowTest.java similarity index 68% rename from identica-common/src/test/java/me/whereareiam/identica/common/verification/DefaultVerificationServiceEnrollmentFlowTest.java rename to identica-feature/feature-verification/verification-common/src/test/java/me/whereareiam/identica/feature/verification/DefaultVerificationServiceEnrollmentFlowTest.java index cea3326a0..07086e1d4 100644 --- a/identica-common/src/test/java/me/whereareiam/identica/common/verification/DefaultVerificationServiceEnrollmentFlowTest.java +++ b/identica-feature/feature-verification/verification-common/src/test/java/me/whereareiam/identica/feature/verification/DefaultVerificationServiceEnrollmentFlowTest.java @@ -1,33 +1,32 @@ -package me.whereareiam.identica.common.verification; +package me.whereareiam.identica.feature.verification; -import me.whereareiam.identica.common.config.defaults.VerificationDefaults; -import me.whereareiam.identica.common.config.defaults.messages.MessagesCommandDefaults; -import me.whereareiam.identica.common.verification.challenge.VerificationChallengeLifecycle; -import me.whereareiam.identica.common.verification.enrollment.VerificationEnrollmentActivator; -import me.whereareiam.identica.common.verification.challenge.VerificationChallengeStore; -import me.whereareiam.identica.common.verification.codec.VerificationStateCodec; -import me.whereareiam.identica.common.verification.enrollment.VerificationEnrollmentLifecycle; -import me.whereareiam.identica.common.verification.enrollment.VerificationEnrollmentStore; -import me.whereareiam.identica.common.verification.resolution.VerificationRequirementResolver; -import me.whereareiam.identica.common.verification.type.totp.TotpCodec; -import me.whereareiam.identica.common.verification.type.totp.TotpVerificationMethod; -import me.whereareiam.identica.common.verification.type.totp.process.TotpChallengeProcess; -import me.whereareiam.identica.common.verification.type.totp.process.TotpEnrollmentProcess; -import me.whereareiam.identica.common.verification.type.totp.state.TotpEnrollmentState; -import me.whereareiam.identica.common.verification.type.totp.step.TotpChallengeVerificationStep; -import me.whereareiam.identica.common.verification.type.totp.step.TotpConfirmCodeStep; -import me.whereareiam.identica.common.verification.type.totp.step.TotpConfirmSavedStep; -import me.whereareiam.identica.common.verification.type.totp.step.TotpSetupStep; -import me.whereareiam.identica.database.VerificationPersistenceService; import me.whereareiam.identica.event.EventManager; +import me.whereareiam.identica.feature.verification.challenge.VerificationChallengeLifecycle; +import me.whereareiam.identica.feature.verification.challenge.VerificationChallengeStore; +import me.whereareiam.identica.feature.verification.challenge.VerificationStateCodec; +import me.whereareiam.identica.feature.verification.config.VerificationMessages; +import me.whereareiam.identica.feature.verification.config.defaults.VerificationDefaults; +import me.whereareiam.identica.feature.verification.database.VerificationPersistenceService; +import me.whereareiam.identica.feature.verification.enrollment.VerificationEnrollmentActivator; +import me.whereareiam.identica.feature.verification.enrollment.VerificationEnrollmentLifecycle; +import me.whereareiam.identica.feature.verification.enrollment.VerificationEnrollmentStore; +import me.whereareiam.identica.feature.verification.model.VerificationRecoveryCode; +import me.whereareiam.identica.feature.verification.model.config.VerificationSettings; +import me.whereareiam.identica.feature.verification.model.enrollment.PendingVerificationEnrollment; +import me.whereareiam.identica.feature.verification.model.enrollment.VerificationEnrollmentResult; +import me.whereareiam.identica.feature.verification.model.interaction.CodeVerificationInteraction; +import me.whereareiam.identica.feature.verification.model.interaction.SavedVerificationInteraction; +import me.whereareiam.identica.feature.verification.resolution.VerificationRequirementResolver; +import me.whereareiam.identica.feature.verification.type.totp.TotpCodec; +import me.whereareiam.identica.feature.verification.type.totp.TotpVerificationMethod; +import me.whereareiam.identica.feature.verification.type.totp.process.TotpChallengeProcess; +import me.whereareiam.identica.feature.verification.type.totp.process.TotpEnrollmentProcess; +import me.whereareiam.identica.feature.verification.type.totp.state.TotpEnrollmentState; +import me.whereareiam.identica.feature.verification.type.totp.step.TotpChallengeVerificationStep; +import me.whereareiam.identica.feature.verification.type.totp.step.TotpConfirmCodeStep; +import me.whereareiam.identica.feature.verification.type.totp.step.TotpConfirmSavedStep; +import me.whereareiam.identica.feature.verification.type.totp.step.TotpSetupStep; import me.whereareiam.identica.identity.session.SessionService; -import me.whereareiam.identica.model.config.Messages; -import me.whereareiam.identica.model.config.Verification; -import me.whereareiam.identica.model.verification.VerificationRecoveryCode; -import me.whereareiam.identica.model.verification.enrollment.PendingVerificationEnrollment; -import me.whereareiam.identica.model.verification.enrollment.VerificationEnrollmentResult; -import me.whereareiam.identica.model.verification.interaction.CodeVerificationInteraction; -import me.whereareiam.identica.model.verification.interaction.SavedVerificationInteraction; import me.whereareiam.identica.provider.ProviderManager; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; @@ -42,20 +41,15 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; class DefaultVerificationServiceEnrollmentFlowTest { - private final Messages commandsMessages = messages(); + private final VerificationMessages commandsMessages = messages(); @Test @SuppressWarnings("unchecked") void beginEnrollmentPersistsStateAndResumesUntilActivation() { - Verification verification = new VerificationDefaults().supply(new Verification()); + VerificationSettings verification = new VerificationDefaults().supply(new VerificationSettings()); verification.setAutoSelectCurrentProvider(false); VerificationPersistenceService persistenceService = mock(VerificationPersistenceService.class); VerificationEnrollmentStore enrollmentStore = mock(VerificationEnrollmentStore.class); @@ -173,7 +167,7 @@ void beginEnrollmentPersistsStateAndResumesUntilActivation() { } private @NotNull TotpVerificationMethod method( - @NotNull Verification verification, + @NotNull VerificationSettings verification, @NotNull VerificationPersistenceService persistenceService ) { TotpSetupStep setupStep = new TotpSetupStep(() -> verification, () -> commandsMessages); @@ -187,11 +181,13 @@ void beginEnrollmentPersistsStateAndResumesUntilActivation() { ); } - private Messages messages() { - Messages messages = new Messages(); - Messages.Commands commands = new Messages.Commands(); - new MessagesCommandDefaults().supply(commands); - messages.setCommands(commands); + private VerificationMessages messages() { + VerificationMessages messages = new VerificationMessages(); + VerificationMessages.Methods.Totp totp = new VerificationMessages.Methods.Totp(); + totp.setPending(List.of("TOTP secret {secret}")); + VerificationMessages.Methods methods = new VerificationMessages.Methods(); + methods.setTotp(totp); + messages.setMethods(methods); return messages; } } diff --git a/identica-common/src/test/java/me/whereareiam/identica/common/verification/DefaultVerificationServiceTest.java b/identica-feature/feature-verification/verification-common/src/test/java/me/whereareiam/identica/feature/verification/DefaultVerificationServiceTest.java similarity index 56% rename from identica-common/src/test/java/me/whereareiam/identica/common/verification/DefaultVerificationServiceTest.java rename to identica-feature/feature-verification/verification-common/src/test/java/me/whereareiam/identica/feature/verification/DefaultVerificationServiceTest.java index 289042d81..ddaadd49d 100644 --- a/identica-common/src/test/java/me/whereareiam/identica/common/verification/DefaultVerificationServiceTest.java +++ b/identica-feature/feature-verification/verification-common/src/test/java/me/whereareiam/identica/feature/verification/DefaultVerificationServiceTest.java @@ -1,18 +1,17 @@ -package me.whereareiam.identica.common.verification; +package me.whereareiam.identica.feature.verification; -import me.whereareiam.identica.common.config.defaults.VerificationDefaults; -import me.whereareiam.identica.common.config.defaults.messages.MessagesCommandDefaults; -import me.whereareiam.identica.common.verification.type.totp.TotpVerificationMethod; -import me.whereareiam.identica.common.verification.type.totp.process.TotpChallengeProcess; -import me.whereareiam.identica.common.verification.type.totp.process.TotpEnrollmentProcess; -import me.whereareiam.identica.common.verification.type.totp.step.TotpChallengeVerificationStep; -import me.whereareiam.identica.common.verification.type.totp.step.TotpConfirmCodeStep; -import me.whereareiam.identica.common.verification.type.totp.step.TotpConfirmSavedStep; -import me.whereareiam.identica.common.verification.type.totp.step.TotpSetupStep; -import me.whereareiam.identica.database.VerificationPersistenceService; -import me.whereareiam.identica.model.config.Messages; -import me.whereareiam.identica.model.config.Verification; -import me.whereareiam.identica.type.verification.VerificationMethodCapability; +import me.whereareiam.identica.feature.verification.config.VerificationMessages; +import me.whereareiam.identica.feature.verification.config.defaults.VerificationDefaults; +import me.whereareiam.identica.feature.verification.database.VerificationPersistenceService; +import me.whereareiam.identica.feature.verification.model.config.VerificationSettings; +import me.whereareiam.identica.feature.verification.type.VerificationMethodCapability; +import me.whereareiam.identica.feature.verification.type.totp.TotpVerificationMethod; +import me.whereareiam.identica.feature.verification.type.totp.process.TotpChallengeProcess; +import me.whereareiam.identica.feature.verification.type.totp.process.TotpEnrollmentProcess; +import me.whereareiam.identica.feature.verification.type.totp.step.TotpChallengeVerificationStep; +import me.whereareiam.identica.feature.verification.type.totp.step.TotpConfirmCodeStep; +import me.whereareiam.identica.feature.verification.type.totp.step.TotpConfirmSavedStep; +import me.whereareiam.identica.feature.verification.type.totp.step.TotpSetupStep; import org.junit.jupiter.api.Test; import java.util.Set; @@ -24,8 +23,8 @@ class DefaultVerificationServiceTest { @Test void registryFindsRegisteredMethodByDescriptorId() { - Verification verification = new VerificationDefaults().supply(new Verification()); - Messages messages = messages(); + VerificationSettings verification = new VerificationDefaults().supply(new VerificationSettings()); + VerificationMessages messages = messages(); VerificationPersistenceService persistenceService = mock(VerificationPersistenceService.class); TotpSetupStep setupStep = new TotpSetupStep(() -> verification, () -> messages); TotpConfirmCodeStep confirmCodeStep = new TotpConfirmCodeStep(() -> verification); @@ -45,8 +44,8 @@ void registryFindsRegisteredMethodByDescriptorId() { @Test void builtInTotpDescriptorAdvertisesProcessCapabilities() { - Verification verification = new VerificationDefaults().supply(new Verification()); - Messages messages = messages(); + VerificationSettings verification = new VerificationDefaults().supply(new VerificationSettings()); + VerificationMessages messages = messages(); VerificationPersistenceService persistenceService = mock(VerificationPersistenceService.class); TotpSetupStep setupStep = new TotpSetupStep(() -> verification, () -> messages); TotpConfirmCodeStep confirmCodeStep = new TotpConfirmCodeStep(() -> verification); @@ -64,11 +63,13 @@ void builtInTotpDescriptorAdvertisesProcessCapabilities() { assertTrue(method.descriptor().getCapabilities().contains(VerificationMethodCapability.RECOVERY_CODES)); } - private Messages messages() { - Messages messages = new Messages(); - Messages.Commands commands = new Messages.Commands(); - new MessagesCommandDefaults().supply(commands); - messages.setCommands(commands); + private VerificationMessages messages() { + VerificationMessages messages = new VerificationMessages(); + VerificationMessages.Methods.Totp totp = new VerificationMessages.Methods.Totp(); + totp.setPending(java.util.List.of("TOTP secret {secret}")); + VerificationMessages.Methods methods = new VerificationMessages.Methods(); + methods.setTotp(totp); + messages.setMethods(methods); return messages; } } diff --git a/identica-adapter-command/src/test/java/me/whereareiam/identica/adapter/command/executor/verification/VerificationResultRendererTest.java b/identica-feature/feature-verification/verification-common/src/test/java/me/whereareiam/identica/feature/verification/command/executor/VerificationResultRendererTest.java similarity index 74% rename from identica-adapter-command/src/test/java/me/whereareiam/identica/adapter/command/executor/verification/VerificationResultRendererTest.java rename to identica-feature/feature-verification/verification-common/src/test/java/me/whereareiam/identica/feature/verification/command/executor/VerificationResultRendererTest.java index a5afbaa4c..eeac36fe5 100644 --- a/identica-adapter-command/src/test/java/me/whereareiam/identica/adapter/command/executor/verification/VerificationResultRendererTest.java +++ b/identica-feature/feature-verification/verification-common/src/test/java/me/whereareiam/identica/feature/verification/command/executor/VerificationResultRendererTest.java @@ -1,15 +1,15 @@ -package me.whereareiam.identica.adapter.command.executor.verification; +package me.whereareiam.identica.feature.verification.command.executor; import me.whereareiam.identica.Serializer; -import me.whereareiam.identica.model.config.Messages; -import me.whereareiam.identica.model.verification.VerificationMethodDescriptor; -import me.whereareiam.identica.model.verification.enrollment.VerificationEnrollmentResult; -import me.whereareiam.identica.model.verification.process.VerificationProcessDisplay; -import me.whereareiam.identica.type.verification.VerificationEnrollmentStatus; -import me.whereareiam.identica.verification.VerificationMethod; -import me.whereareiam.identica.verification.VerificationRegistry; -import me.whereareiam.identica.verification.process.VerificationChallengeProcess; -import me.whereareiam.identica.verification.process.VerificationEnrollmentProcess; +import me.whereareiam.identica.feature.verification.VerificationMethod; +import me.whereareiam.identica.feature.verification.VerificationRegistry; +import me.whereareiam.identica.feature.verification.config.VerificationMessages; +import me.whereareiam.identica.feature.verification.model.VerificationMethodDescriptor; +import me.whereareiam.identica.feature.verification.model.enrollment.VerificationEnrollmentResult; +import me.whereareiam.identica.feature.verification.model.process.VerificationProcessDisplay; +import me.whereareiam.identica.feature.verification.process.VerificationChallengeProcess; +import me.whereareiam.identica.feature.verification.process.VerificationEnrollmentProcess; +import me.whereareiam.identica.feature.verification.type.status.VerificationEnrollmentStatus; import me.whereareiam.keystone.Actor; import me.whereareiam.keystone.model.SerializerContent; import me.whereareiam.keystone.model.SerializerOptions; @@ -22,12 +22,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; +import java.util.*; import java.util.concurrent.atomic.AtomicReference; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -48,7 +43,7 @@ static void initializeSerializer() { @Override public @NotNull Component serialize(@NotNull SerializerContent content) { - String message = content.getMessage() == null ? "" : content.getMessage(); + String message = content.getMessage(); for (Map.Entry entry : content.getPlaceholders().entrySet()) { message = message.replace("{" + entry.getKey() + "}", entry.getValue() == null ? "" : entry.getValue()); } @@ -97,16 +92,15 @@ void failsFastWhenEnrollmentDisplayMissing() { assertEquals("Verification method mail did not provide enrollment display", error.getMessage()); } - private Messages messages() { - Messages messages = new Messages(); - Messages.Commands commands = new Messages.Commands(); - Messages.Commands.Verification verification = new Messages.Commands.Verification(); - Messages.Commands.Verification.Enroll enroll = new Messages.Commands.Verification.Enroll(); + private VerificationMessages messages() { + VerificationMessages messages = new VerificationMessages(); + VerificationMessages.Commands commands = new VerificationMessages.Commands(); + VerificationMessages.Commands.Enroll enroll = new VerificationMessages.Commands.Enroll(); enroll.setUnknownMethod("unknown"); enroll.setAlreadyEnrolled("already"); - verification.setEnroll(enroll); + commands.setEnroll(enroll); - Messages.Commands.Verification.Confirm confirm = new Messages.Commands.Verification.Confirm(); + VerificationMessages.Commands.Confirm confirm = new VerificationMessages.Commands.Confirm(); confirm.setNoPending("no-pending"); confirm.setInvalidCode("invalid"); confirm.setProtectedActionSelectionRequired("selection-required"); @@ -114,18 +108,18 @@ private Messages messages() { confirm.setMethodUnavailable("method-unavailable"); confirm.setEnabled("enabled"); confirm.setAutoSelected("auto-selected"); - Messages.Commands.Verification.Confirm.RecoveryCodes recoveryCodes = new Messages.Commands.Verification.Confirm.RecoveryCodes(); + VerificationMessages.Commands.Confirm.RecoveryCodes recoveryCodes = new VerificationMessages.Commands.Confirm.RecoveryCodes(); recoveryCodes.setBody(List.of("{entries}")); - Messages.Commands.EntryFormat entryFormat = new Messages.Commands.EntryFormat(); + VerificationMessages.Commands.EntryFormat entryFormat = new VerificationMessages.Commands.EntryFormat(); entryFormat.setFormat("{code}"); entryFormat.setEmptyFormat("{code}"); recoveryCodes.setSingleColumnEntry(entryFormat); recoveryCodes.setTwoColumnEntry(entryFormat); recoveryCodes.setEmpty("empty"); confirm.setRecoveryCodes(recoveryCodes); - verification.setConfirm(confirm); + commands.setConfirm(confirm); - Messages.Commands.Verification.Use use = new Messages.Commands.Verification.Use(); + VerificationMessages.Commands.Use use = new VerificationMessages.Commands.Use(); use.setProviderNotFound("provider-not-found"); use.setProviderUnsupported("provider-unsupported"); use.setProviderVerificationDisabled("provider-verification-disabled"); @@ -133,23 +127,21 @@ private Messages messages() { use.setMethodDisabledForProvider("method-disabled-for-provider"); use.setAlreadySelected("already-selected"); use.setUpdated("updated"); - verification.setUse(use); + commands.setUse(use); - Messages.Commands.Verification.Disable disable = new Messages.Commands.Verification.Disable(); + VerificationMessages.Commands.Disable disable = new VerificationMessages.Commands.Disable(); disable.setMethodNotEnrolled("disable-method-not-enrolled"); disable.setProtectedPrompt("disable-protected-prompt"); disable.setDisabled("disabled"); - verification.setDisable(disable); + commands.setDisable(disable); - Messages.Commands.Verification.Cancel cancel = new Messages.Commands.Verification.Cancel(); + VerificationMessages.Commands.Cancel cancel = new VerificationMessages.Commands.Cancel(); cancel.setNoPending("cancel-no-pending"); cancel.setCancelled("cancelled"); cancel.setCancelledProtectedAction("cancelled-protected"); - verification.setCancel(cancel); - verification.setPlayerOnly("player-only"); - verification.setNotAllowed("not-allowed"); - - commands.setVerification(verification); + commands.setCancel(cancel); + commands.setPlayerOnly("player-only"); + commands.setNotAllowed("not-allowed"); messages.setCommands(commands); return messages; } diff --git a/identica-common/src/test/java/me/whereareiam/identica/common/verification/process/VerificationProcessOrchestratorTest.java b/identica-feature/feature-verification/verification-common/src/test/java/me/whereareiam/identica/feature/verification/process/VerificationProcessOrchestratorTest.java similarity index 86% rename from identica-common/src/test/java/me/whereareiam/identica/common/verification/process/VerificationProcessOrchestratorTest.java rename to identica-feature/feature-verification/verification-common/src/test/java/me/whereareiam/identica/feature/verification/process/VerificationProcessOrchestratorTest.java index 318b48c47..9f7537f95 100644 --- a/identica-common/src/test/java/me/whereareiam/identica/common/verification/process/VerificationProcessOrchestratorTest.java +++ b/identica-feature/feature-verification/verification-common/src/test/java/me/whereareiam/identica/feature/verification/process/VerificationProcessOrchestratorTest.java @@ -1,13 +1,11 @@ -package me.whereareiam.identica.common.verification.process; - -import me.whereareiam.identica.model.verification.interaction.CodeVerificationInteraction; -import me.whereareiam.identica.model.verification.process.VerificationProcessContext; -import me.whereareiam.identica.model.verification.process.VerificationProcessResult; -import me.whereareiam.identica.model.verification.process.VerificationProcessState; -import me.whereareiam.identica.model.verification.process.VerificationProcessTransition; -import me.whereareiam.identica.verification.process.base.VerificationProcessOrchestrator; -import me.whereareiam.identica.verification.process.VerificationProcessStep; -import me.whereareiam.identica.verification.process.VerificationStepCursor; +package me.whereareiam.identica.feature.verification.process; + +import me.whereareiam.identica.feature.verification.model.interaction.CodeVerificationInteraction; +import me.whereareiam.identica.feature.verification.model.process.VerificationProcessContext; +import me.whereareiam.identica.feature.verification.model.process.VerificationProcessResult; +import me.whereareiam.identica.feature.verification.model.process.VerificationProcessTransition; +import me.whereareiam.identica.feature.verification.process.base.VerificationProcessOrchestrator; +import me.whereareiam.identica.feature.verification.state.VerificationProcessState; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; diff --git a/identica-common/src/test/java/me/whereareiam/identica/common/verification/type/totp/process/TotpEnrollmentProcessTest.java b/identica-feature/feature-verification/verification-common/src/test/java/me/whereareiam/identica/feature/verification/type/totp/process/TotpEnrollmentProcessTest.java similarity index 71% rename from identica-common/src/test/java/me/whereareiam/identica/common/verification/type/totp/process/TotpEnrollmentProcessTest.java rename to identica-feature/feature-verification/verification-common/src/test/java/me/whereareiam/identica/feature/verification/type/totp/process/TotpEnrollmentProcessTest.java index 4eb6ab6ab..931e8196c 100644 --- a/identica-common/src/test/java/me/whereareiam/identica/common/verification/type/totp/process/TotpEnrollmentProcessTest.java +++ b/identica-feature/feature-verification/verification-common/src/test/java/me/whereareiam/identica/feature/verification/type/totp/process/TotpEnrollmentProcessTest.java @@ -1,36 +1,31 @@ -package me.whereareiam.identica.common.verification.type.totp.process; - -import me.whereareiam.identica.common.config.defaults.VerificationDefaults; -import me.whereareiam.identica.common.config.defaults.messages.MessagesCommandDefaults; -import me.whereareiam.identica.common.verification.type.totp.TotpCodec; -import me.whereareiam.identica.common.verification.type.totp.state.TotpEnrollmentState; -import me.whereareiam.identica.common.verification.type.totp.step.TotpConfirmCodeStep; -import me.whereareiam.identica.common.verification.type.totp.step.TotpConfirmSavedStep; -import me.whereareiam.identica.common.verification.type.totp.step.TotpSetupStep; -import me.whereareiam.identica.model.config.Messages; -import me.whereareiam.identica.model.config.Verification; -import me.whereareiam.identica.model.verification.enrollment.VerificationEnrollmentContext; -import me.whereareiam.identica.model.verification.enrollment.VerificationEnrollmentResult; -import me.whereareiam.identica.model.verification.interaction.CodeVerificationInteraction; -import me.whereareiam.identica.model.verification.interaction.SavedVerificationInteraction; -import me.whereareiam.identica.model.verification.process.VerificationProcessContext; -import me.whereareiam.identica.model.verification.process.VerificationProcessResult; -import me.whereareiam.identica.model.verification.process.VerificationProcessTransition; +package me.whereareiam.identica.feature.verification.type.totp.process; + +import me.whereareiam.identica.feature.verification.config.VerificationMessages; +import me.whereareiam.identica.feature.verification.config.defaults.VerificationDefaults; +import me.whereareiam.identica.feature.verification.model.config.VerificationSettings; +import me.whereareiam.identica.feature.verification.model.enrollment.VerificationEnrollmentContext; +import me.whereareiam.identica.feature.verification.model.enrollment.VerificationEnrollmentResult; +import me.whereareiam.identica.feature.verification.model.interaction.CodeVerificationInteraction; +import me.whereareiam.identica.feature.verification.model.interaction.SavedVerificationInteraction; +import me.whereareiam.identica.feature.verification.model.process.VerificationProcessContext; +import me.whereareiam.identica.feature.verification.model.process.VerificationProcessResult; +import me.whereareiam.identica.feature.verification.model.process.VerificationProcessTransition; +import me.whereareiam.identica.feature.verification.type.totp.TotpCodec; +import me.whereareiam.identica.feature.verification.type.totp.state.TotpEnrollmentState; +import me.whereareiam.identica.feature.verification.type.totp.step.TotpConfirmCodeStep; +import me.whereareiam.identica.feature.verification.type.totp.step.TotpConfirmSavedStep; +import me.whereareiam.identica.feature.verification.type.totp.step.TotpSetupStep; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import java.util.UUID; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; class TotpEnrollmentProcessTest { @Test void startStoresNextCursorAfterSetupStep() { - Verification verification = verification(); + VerificationSettings verification = verification(); TotpEnrollmentProcess process = process(verification); VerificationEnrollmentResult result = process.start(context(UUID.randomUUID(), null)); @@ -45,7 +40,7 @@ void startStoresNextCursorAfterSetupStep() { @Test void invalidCodeStaysOnConfirmCodeStep() { - Verification verification = verification(); + VerificationSettings verification = verification(); TotpEnrollmentProcess process = process(verification); UUID subjectUniqueId = UUID.randomUUID(); VerificationEnrollmentResult started = process.start(context(subjectUniqueId, null)); @@ -68,7 +63,7 @@ void invalidCodeStaysOnConfirmCodeStep() { @Test void validCodeAdvancesToSavedConfirmation() { - Verification verification = verification(); + VerificationSettings verification = verification(); TotpEnrollmentProcess process = process(verification); UUID subjectUniqueId = UUID.randomUUID(); VerificationEnrollmentResult started = process.start(context(subjectUniqueId, null)); @@ -95,7 +90,7 @@ void validCodeAdvancesToSavedConfirmation() { @Test void savedConfirmationCompletesWithoutChangingCursor() { - Verification verification = verification(); + VerificationSettings verification = verification(); TotpEnrollmentProcess process = process(verification); UUID subjectUniqueId = UUID.randomUUID(); VerificationEnrollmentResult started = process.start(context(subjectUniqueId, null)); @@ -116,7 +111,7 @@ void savedConfirmationCompletesWithoutChangingCursor() { @Test void gotoTransitionJumpsToExplicitStep() { - Verification verification = verification(); + VerificationSettings verification = verification(); TotpSetupStep setupStep = new TotpSetupStep(() -> verification, this::messages); TotpConfirmSavedStep confirmSavedStep = new TotpConfirmSavedStep(); TotpEnrollmentProcess process = new TotpEnrollmentProcess( @@ -144,7 +139,7 @@ void gotoTransitionJumpsToExplicitStep() { private @NotNull TotpEnrollmentState confirmCode( @NotNull TotpEnrollmentProcess process, - @NotNull Verification verification, + @NotNull VerificationSettings verification, @NotNull UUID subjectUniqueId, @NotNull TotpEnrollmentState state ) { @@ -163,7 +158,7 @@ void gotoTransitionJumpsToExplicitStep() { ).getState(); } - private @NotNull TotpEnrollmentProcess process(@NotNull Verification verification) { + private @NotNull TotpEnrollmentProcess process(@NotNull VerificationSettings verification) { return new TotpEnrollmentProcess( new TotpSetupStep(() -> verification, this::messages), new TotpConfirmCodeStep(() -> verification), @@ -185,22 +180,24 @@ void gotoTransitionJumpsToExplicitStep() { .build(); } - private @NotNull Verification verification() { - return new VerificationDefaults().supply(new Verification()); + private @NotNull VerificationSettings verification() { + return new VerificationDefaults().supply(new VerificationSettings()); } - private @NotNull Messages messages() { - Messages messages = new Messages(); - Messages.Commands commands = new Messages.Commands(); - new MessagesCommandDefaults().supply(commands); - messages.setCommands(commands); + private @NotNull VerificationMessages messages() { + VerificationMessages messages = new VerificationMessages(); + VerificationMessages.Methods.Totp totp = new VerificationMessages.Methods.Totp(); + totp.setPending(java.util.List.of("TOTP secret {secret}")); + VerificationMessages.Methods methods = new VerificationMessages.Methods(); + methods.setTotp(totp); + messages.setMethods(methods); return messages; } private static final class RedirectingConfirmCodeStep extends TotpConfirmCodeStep { private final @NotNull String targetStepId; - private RedirectingConfirmCodeStep(@NotNull Verification verification, @NotNull String targetStepId) { + private RedirectingConfirmCodeStep(@NotNull VerificationSettings verification, @NotNull String targetStepId) { super(() -> verification); this.targetStepId = targetStepId; } diff --git a/identica-feature/feature-verification/verification-database/build.gradle.kts b/identica-feature/feature-verification/verification-database/build.gradle.kts new file mode 100644 index 000000000..ff9e12c4f --- /dev/null +++ b/identica-feature/feature-verification/verification-database/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + id("shared") +} + +dependencies { + implementation(projects.featureVerificationApi) + compileOnly(libs.jdbi.core) + compileOnly(libs.jdbi.sqlobject) + + testImplementation(libs.jdbi.core) + testImplementation(libs.jdbi.sqlobject) +} diff --git a/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/verification/DefaultVerificationPersistenceService.java b/identica-feature/feature-verification/verification-database/src/main/java/me/whereareiam/identica/feature/verification/database/DefaultVerificationPersistenceService.java similarity index 86% rename from identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/verification/DefaultVerificationPersistenceService.java rename to identica-feature/feature-verification/verification-database/src/main/java/me/whereareiam/identica/feature/verification/database/DefaultVerificationPersistenceService.java index 80700c166..03e040565 100644 --- a/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/verification/DefaultVerificationPersistenceService.java +++ b/identica-feature/feature-verification/verification-database/src/main/java/me/whereareiam/identica/feature/verification/database/DefaultVerificationPersistenceService.java @@ -1,21 +1,20 @@ -package me.whereareiam.identica.adapter.database.verification; +package me.whereareiam.identica.feature.verification.database; import com.google.inject.Inject; import com.google.inject.Singleton; -import me.whereareiam.identica.adapter.database.mapper.verification.VerificationEnrollmentMapper; -import me.whereareiam.identica.adapter.database.mapper.verification.VerificationRecoveryCodeMapper; -import me.whereareiam.identica.adapter.database.mapper.verification.VerificationSelectionMapper; -import me.whereareiam.identica.adapter.database.repository.verification.VerificationEnrollmentRepository; -import me.whereareiam.identica.adapter.database.repository.verification.VerificationRecoveryCodeRepository; -import me.whereareiam.identica.adapter.database.repository.verification.VerificationSelectionRepository; -import me.whereareiam.identica.database.VerificationPersistenceService; import me.whereareiam.identica.event.EventListener; import me.whereareiam.identica.event.EventManager; import me.whereareiam.identica.event.account.AccountLifecycleEvent; import me.whereareiam.identica.event.base.IdenticEvent; -import me.whereareiam.identica.model.verification.VerificationRecoveryCode; -import me.whereareiam.identica.model.verification.enrollment.VerificationEnrollment; -import me.whereareiam.identica.model.verification.selection.VerificationSelection; +import me.whereareiam.identica.feature.verification.database.mapper.VerificationEnrollmentMapper; +import me.whereareiam.identica.feature.verification.database.mapper.VerificationRecoveryCodeMapper; +import me.whereareiam.identica.feature.verification.database.mapper.VerificationSelectionMapper; +import me.whereareiam.identica.feature.verification.database.repository.VerificationEnrollmentRepository; +import me.whereareiam.identica.feature.verification.database.repository.VerificationRecoveryCodeRepository; +import me.whereareiam.identica.feature.verification.database.repository.VerificationSelectionRepository; +import me.whereareiam.identica.feature.verification.model.VerificationRecoveryCode; +import me.whereareiam.identica.feature.verification.model.enrollment.VerificationEnrollment; +import me.whereareiam.identica.feature.verification.model.selection.VerificationSelection; import me.whereareiam.identica.type.event.EventOrder; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-feature/feature-verification/verification-database/src/main/java/me/whereareiam/identica/feature/verification/database/VerificationDatabaseConfiguration.java b/identica-feature/feature-verification/verification-database/src/main/java/me/whereareiam/identica/feature/verification/database/VerificationDatabaseConfiguration.java new file mode 100644 index 000000000..97cc0559b --- /dev/null +++ b/identica-feature/feature-verification/verification-database/src/main/java/me/whereareiam/identica/feature/verification/database/VerificationDatabaseConfiguration.java @@ -0,0 +1,34 @@ +package me.whereareiam.identica.feature.verification.database; + +import com.google.inject.AbstractModule; +import com.google.inject.Provides; +import com.google.inject.Singleton; +import me.whereareiam.identica.feature.verification.database.repository.VerificationEnrollmentRepository; +import me.whereareiam.identica.feature.verification.database.repository.VerificationRecoveryCodeRepository; +import me.whereareiam.identica.feature.verification.database.repository.VerificationSelectionRepository; +import org.jdbi.v3.core.Jdbi; + +public class VerificationDatabaseConfiguration extends AbstractModule { + @Override + protected void configure() { + bind(VerificationPersistenceService.class).to(DefaultVerificationPersistenceService.class).asEagerSingleton(); + } + + @Provides + @Singleton + public VerificationEnrollmentRepository provideVerificationEnrollmentRepository(Jdbi jdbi) { + return jdbi.onDemand(VerificationEnrollmentRepository.class); + } + + @Provides + @Singleton + public VerificationSelectionRepository provideVerificationSelectionRepository(Jdbi jdbi) { + return jdbi.onDemand(VerificationSelectionRepository.class); + } + + @Provides + @Singleton + public VerificationRecoveryCodeRepository provideVerificationRecoveryCodeRepository(Jdbi jdbi) { + return jdbi.onDemand(VerificationRecoveryCodeRepository.class); + } +} diff --git a/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/entity/verification/VerificationEnrollmentEntity.java b/identica-feature/feature-verification/verification-database/src/main/java/me/whereareiam/identica/feature/verification/database/entity/VerificationEnrollmentEntity.java similarity index 87% rename from identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/entity/verification/VerificationEnrollmentEntity.java rename to identica-feature/feature-verification/verification-database/src/main/java/me/whereareiam/identica/feature/verification/database/entity/VerificationEnrollmentEntity.java index 794467050..fe97fdfa6 100644 --- a/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/entity/verification/VerificationEnrollmentEntity.java +++ b/identica-feature/feature-verification/verification-database/src/main/java/me/whereareiam/identica/feature/verification/database/entity/VerificationEnrollmentEntity.java @@ -1,10 +1,6 @@ -package me.whereareiam.identica.adapter.database.entity.verification; +package me.whereareiam.identica.feature.verification.database.entity; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.*; import me.whereareiam.dialectica.EntitySchemaProvider; import me.whereareiam.dialectica.annotation.Entity; diff --git a/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/entity/verification/VerificationRecoveryCodeEntity.java b/identica-feature/feature-verification/verification-database/src/main/java/me/whereareiam/identica/feature/verification/database/entity/VerificationRecoveryCodeEntity.java similarity index 85% rename from identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/entity/verification/VerificationRecoveryCodeEntity.java rename to identica-feature/feature-verification/verification-database/src/main/java/me/whereareiam/identica/feature/verification/database/entity/VerificationRecoveryCodeEntity.java index e15db4579..040a6d493 100644 --- a/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/entity/verification/VerificationRecoveryCodeEntity.java +++ b/identica-feature/feature-verification/verification-database/src/main/java/me/whereareiam/identica/feature/verification/database/entity/VerificationRecoveryCodeEntity.java @@ -1,10 +1,6 @@ -package me.whereareiam.identica.adapter.database.entity.verification; +package me.whereareiam.identica.feature.verification.database.entity; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.*; import me.whereareiam.dialectica.EntitySchemaProvider; import me.whereareiam.dialectica.annotation.Entity; diff --git a/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/entity/verification/VerificationSelectionEntity.java b/identica-feature/feature-verification/verification-database/src/main/java/me/whereareiam/identica/feature/verification/database/entity/VerificationSelectionEntity.java similarity index 85% rename from identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/entity/verification/VerificationSelectionEntity.java rename to identica-feature/feature-verification/verification-database/src/main/java/me/whereareiam/identica/feature/verification/database/entity/VerificationSelectionEntity.java index 2f751f084..5def72068 100644 --- a/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/entity/verification/VerificationSelectionEntity.java +++ b/identica-feature/feature-verification/verification-database/src/main/java/me/whereareiam/identica/feature/verification/database/entity/VerificationSelectionEntity.java @@ -1,10 +1,6 @@ -package me.whereareiam.identica.adapter.database.entity.verification; +package me.whereareiam.identica.feature.verification.database.entity; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.*; import me.whereareiam.dialectica.EntitySchemaProvider; import me.whereareiam.dialectica.annotation.Entity; diff --git a/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/mapper/verification/VerificationEnrollmentMapper.java b/identica-feature/feature-verification/verification-database/src/main/java/me/whereareiam/identica/feature/verification/database/mapper/VerificationEnrollmentMapper.java similarity index 76% rename from identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/mapper/verification/VerificationEnrollmentMapper.java rename to identica-feature/feature-verification/verification-database/src/main/java/me/whereareiam/identica/feature/verification/database/mapper/VerificationEnrollmentMapper.java index 136ba4475..341f7836c 100644 --- a/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/mapper/verification/VerificationEnrollmentMapper.java +++ b/identica-feature/feature-verification/verification-database/src/main/java/me/whereareiam/identica/feature/verification/database/mapper/VerificationEnrollmentMapper.java @@ -1,7 +1,7 @@ -package me.whereareiam.identica.adapter.database.mapper.verification; +package me.whereareiam.identica.feature.verification.database.mapper; -import me.whereareiam.identica.adapter.database.entity.verification.VerificationEnrollmentEntity; -import me.whereareiam.identica.model.verification.enrollment.VerificationEnrollment; +import me.whereareiam.identica.feature.verification.database.entity.VerificationEnrollmentEntity; +import me.whereareiam.identica.feature.verification.model.enrollment.VerificationEnrollment; public final class VerificationEnrollmentMapper { public static VerificationEnrollment toModel(VerificationEnrollmentEntity entity) { diff --git a/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/mapper/verification/VerificationRecoveryCodeMapper.java b/identica-feature/feature-verification/verification-database/src/main/java/me/whereareiam/identica/feature/verification/database/mapper/VerificationRecoveryCodeMapper.java similarity index 73% rename from identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/mapper/verification/VerificationRecoveryCodeMapper.java rename to identica-feature/feature-verification/verification-database/src/main/java/me/whereareiam/identica/feature/verification/database/mapper/VerificationRecoveryCodeMapper.java index dfb314c25..e8bb5ff25 100644 --- a/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/mapper/verification/VerificationRecoveryCodeMapper.java +++ b/identica-feature/feature-verification/verification-database/src/main/java/me/whereareiam/identica/feature/verification/database/mapper/VerificationRecoveryCodeMapper.java @@ -1,7 +1,7 @@ -package me.whereareiam.identica.adapter.database.mapper.verification; +package me.whereareiam.identica.feature.verification.database.mapper; -import me.whereareiam.identica.adapter.database.entity.verification.VerificationRecoveryCodeEntity; -import me.whereareiam.identica.model.verification.VerificationRecoveryCode; +import me.whereareiam.identica.feature.verification.database.entity.VerificationRecoveryCodeEntity; +import me.whereareiam.identica.feature.verification.model.VerificationRecoveryCode; public final class VerificationRecoveryCodeMapper { public static VerificationRecoveryCode toModel(VerificationRecoveryCodeEntity entity) { diff --git a/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/mapper/verification/VerificationSelectionMapper.java b/identica-feature/feature-verification/verification-database/src/main/java/me/whereareiam/identica/feature/verification/database/mapper/VerificationSelectionMapper.java similarity index 71% rename from identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/mapper/verification/VerificationSelectionMapper.java rename to identica-feature/feature-verification/verification-database/src/main/java/me/whereareiam/identica/feature/verification/database/mapper/VerificationSelectionMapper.java index 4de9c8e44..94e7fd27e 100644 --- a/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/mapper/verification/VerificationSelectionMapper.java +++ b/identica-feature/feature-verification/verification-database/src/main/java/me/whereareiam/identica/feature/verification/database/mapper/VerificationSelectionMapper.java @@ -1,7 +1,7 @@ -package me.whereareiam.identica.adapter.database.mapper.verification; +package me.whereareiam.identica.feature.verification.database.mapper; -import me.whereareiam.identica.adapter.database.entity.verification.VerificationSelectionEntity; -import me.whereareiam.identica.model.verification.selection.VerificationSelection; +import me.whereareiam.identica.feature.verification.database.entity.VerificationSelectionEntity; +import me.whereareiam.identica.feature.verification.model.selection.VerificationSelection; public final class VerificationSelectionMapper { public static VerificationSelection toModel(VerificationSelectionEntity entity) { diff --git a/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/repository/verification/VerificationEnrollmentRepository.java b/identica-feature/feature-verification/verification-database/src/main/java/me/whereareiam/identica/feature/verification/database/repository/VerificationEnrollmentRepository.java similarity index 95% rename from identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/repository/verification/VerificationEnrollmentRepository.java rename to identica-feature/feature-verification/verification-database/src/main/java/me/whereareiam/identica/feature/verification/database/repository/VerificationEnrollmentRepository.java index 81a61d935..b01fd071d 100644 --- a/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/repository/verification/VerificationEnrollmentRepository.java +++ b/identica-feature/feature-verification/verification-database/src/main/java/me/whereareiam/identica/feature/verification/database/repository/VerificationEnrollmentRepository.java @@ -1,6 +1,6 @@ -package me.whereareiam.identica.adapter.database.repository.verification; +package me.whereareiam.identica.feature.verification.database.repository; -import me.whereareiam.identica.adapter.database.entity.verification.VerificationEnrollmentEntity; +import me.whereareiam.identica.feature.verification.database.entity.VerificationEnrollmentEntity; import org.jdbi.v3.sqlobject.config.RegisterBeanMapper; import org.jdbi.v3.sqlobject.customizer.Bind; import org.jdbi.v3.sqlobject.statement.SqlQuery; diff --git a/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/repository/verification/VerificationRecoveryCodeRepository.java b/identica-feature/feature-verification/verification-database/src/main/java/me/whereareiam/identica/feature/verification/database/repository/VerificationRecoveryCodeRepository.java similarity index 91% rename from identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/repository/verification/VerificationRecoveryCodeRepository.java rename to identica-feature/feature-verification/verification-database/src/main/java/me/whereareiam/identica/feature/verification/database/repository/VerificationRecoveryCodeRepository.java index 84f27525f..6dae6eb4f 100644 --- a/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/repository/verification/VerificationRecoveryCodeRepository.java +++ b/identica-feature/feature-verification/verification-database/src/main/java/me/whereareiam/identica/feature/verification/database/repository/VerificationRecoveryCodeRepository.java @@ -1,6 +1,6 @@ -package me.whereareiam.identica.adapter.database.repository.verification; +package me.whereareiam.identica.feature.verification.database.repository; -import me.whereareiam.identica.adapter.database.entity.verification.VerificationRecoveryCodeEntity; +import me.whereareiam.identica.feature.verification.database.entity.VerificationRecoveryCodeEntity; import org.jdbi.v3.sqlobject.config.RegisterBeanMapper; import org.jdbi.v3.sqlobject.customizer.Bind; import org.jdbi.v3.sqlobject.statement.SqlQuery; diff --git a/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/repository/verification/VerificationSelectionRepository.java b/identica-feature/feature-verification/verification-database/src/main/java/me/whereareiam/identica/feature/verification/database/repository/VerificationSelectionRepository.java similarity index 94% rename from identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/repository/verification/VerificationSelectionRepository.java rename to identica-feature/feature-verification/verification-database/src/main/java/me/whereareiam/identica/feature/verification/database/repository/VerificationSelectionRepository.java index 05f0cdd7c..e0b3323f4 100644 --- a/identica-adapter-database/src/main/java/me/whereareiam/identica/adapter/database/repository/verification/VerificationSelectionRepository.java +++ b/identica-feature/feature-verification/verification-database/src/main/java/me/whereareiam/identica/feature/verification/database/repository/VerificationSelectionRepository.java @@ -1,6 +1,6 @@ -package me.whereareiam.identica.adapter.database.repository.verification; +package me.whereareiam.identica.feature.verification.database.repository; -import me.whereareiam.identica.adapter.database.entity.verification.VerificationSelectionEntity; +import me.whereareiam.identica.feature.verification.database.entity.VerificationSelectionEntity; import org.jdbi.v3.sqlobject.config.RegisterBeanMapper; import org.jdbi.v3.sqlobject.customizer.Bind; import org.jdbi.v3.sqlobject.statement.SqlQuery; diff --git a/identica-feature/feature-verification/verification/build.gradle.kts b/identica-feature/feature-verification/verification/build.gradle.kts new file mode 100644 index 000000000..3479005c4 --- /dev/null +++ b/identica-feature/feature-verification/verification/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + id("shared") +} + +dependencies { + api(projects.featureVerificationApi) + implementation(projects.featureVerificationCommon) + implementation(projects.featureVerificationDatabase) +} diff --git a/identica-feature/feature-verification/verification/src/main/java/me/whereareiam/identica/feature/verification/VerificationFeatureConfiguration.java b/identica-feature/feature-verification/verification/src/main/java/me/whereareiam/identica/feature/verification/VerificationFeatureConfiguration.java new file mode 100644 index 000000000..f9083c4e2 --- /dev/null +++ b/identica-feature/feature-verification/verification/src/main/java/me/whereareiam/identica/feature/verification/VerificationFeatureConfiguration.java @@ -0,0 +1,24 @@ +package me.whereareiam.identica.feature.verification; + +import com.google.inject.AbstractModule; +import com.google.inject.Provides; +import com.google.inject.Singleton; +import com.google.inject.name.Named; +import me.whereareiam.identica.feature.verification.database.VerificationDatabaseConfiguration; + +import java.nio.file.Path; + +public class VerificationFeatureConfiguration extends AbstractModule { + @Override + protected void configure() { + install(new VerificationCommonConfiguration()); + install(new VerificationDatabaseConfiguration()); + } + + @Provides + @Singleton + @Named("verificationFeaturePath") + Path provideVerificationFeaturePath(@Named("featuresPath") Path featuresPath) { + return featuresPath.resolve("verification"); + } +} diff --git a/identica-integration/integration-bstats/build.gradle.kts b/identica-integration/integration-bstats/build.gradle.kts index d20043c49..00e58c499 100644 --- a/identica-integration/integration-bstats/build.gradle.kts +++ b/identica-integration/integration-bstats/build.gradle.kts @@ -3,5 +3,6 @@ plugins { } dependencies { + compileOnly(projects.featureVerificationApi) compileOnly(libs.bStats) } diff --git a/identica-integration/integration-bstats/src/main/java/me/whereareiam/identica/integration/bstats/chart/verification/VerificationMethodsChart.java b/identica-integration/integration-bstats/src/main/java/me/whereareiam/identica/integration/bstats/chart/verification/VerificationMethodsChart.java index ad8576a91..4368a35f7 100644 --- a/identica-integration/integration-bstats/src/main/java/me/whereareiam/identica/integration/bstats/chart/verification/VerificationMethodsChart.java +++ b/identica-integration/integration-bstats/src/main/java/me/whereareiam/identica/integration/bstats/chart/verification/VerificationMethodsChart.java @@ -2,10 +2,10 @@ import com.google.inject.Inject; import com.google.inject.Singleton; +import me.whereareiam.identica.feature.verification.VerificationMethod; +import me.whereareiam.identica.feature.verification.VerificationRegistry; +import me.whereareiam.identica.feature.verification.model.VerificationMethodDescriptor; import me.whereareiam.identica.integration.bstats.chart.type.NamedDrilldownPieChart; -import me.whereareiam.identica.model.verification.VerificationMethodDescriptor; -import me.whereareiam.identica.verification.VerificationMethod; -import me.whereareiam.identica.verification.VerificationRegistry; import org.jetbrains.annotations.NotNull; import java.util.LinkedHashMap; diff --git a/identica-platform/platform-bungeecord/bootstrap/build.gradle.kts b/identica-platform/platform-bungeecord/bootstrap/build.gradle.kts index fbcee2c5f..de9d6eba0 100644 --- a/identica-platform/platform-bungeecord/bootstrap/build.gradle.kts +++ b/identica-platform/platform-bungeecord/bootstrap/build.gradle.kts @@ -13,6 +13,7 @@ dependencies { testImplementation(libs.cloud.bungee) implementation(projects.platformBungeecordApi) + implementation(projects.featureVerification) implementation(libs.bundles.bStats.bungeecord) implementation(libs.attache.bungeecord) diff --git a/identica-platform/platform-bungeecord/bootstrap/src/main/java/me/whereareiam/identica/platform/bungeecord/BungeeCordIdentica.java b/identica-platform/platform-bungeecord/bootstrap/src/main/java/me/whereareiam/identica/platform/bungeecord/BungeeCordIdentica.java index d6fd74861..8f1e8dccd 100644 --- a/identica-platform/platform-bungeecord/bootstrap/src/main/java/me/whereareiam/identica/platform/bungeecord/BungeeCordIdentica.java +++ b/identica-platform/platform-bungeecord/bootstrap/src/main/java/me/whereareiam/identica/platform/bungeecord/BungeeCordIdentica.java @@ -13,6 +13,7 @@ import me.whereareiam.identica.event.lifecycle.IdenticaBootstrappedEvent; import me.whereareiam.identica.event.lifecycle.IdenticaReadyEvent; import me.whereareiam.identica.event.lifecycle.IdenticaShutdownEvent; +import me.whereareiam.identica.feature.verification.VerificationFeatureConfiguration; import me.whereareiam.identica.platform.bungeecord.logging.BungeeCordLoggingHelper; import me.whereareiam.identica.type.PluginType; import net.kyori.adventure.platform.bungeecord.BungeeAudiences; @@ -34,6 +35,7 @@ public void onEnable() { audiences = BungeeAudiences.builder(this).build(); injector = Guice.createInjector( new CommonConfiguration(getDataFolder().toPath()), + new VerificationFeatureConfiguration(), new EngineConfiguration(), new BungeeCordConfiguration(getProxy(), this, getDataFolder().toPath(), audiences), new CommandConfiguration(), diff --git a/identica-platform/platform-bungeecord/bootstrap/src/main/java/me/whereareiam/identica/platform/bungeecord/listener/connection/PlayerDisconnectListener.java b/identica-platform/platform-bungeecord/bootstrap/src/main/java/me/whereareiam/identica/platform/bungeecord/listener/connection/PlayerDisconnectListener.java index 213756a5f..be7873690 100644 --- a/identica-platform/platform-bungeecord/bootstrap/src/main/java/me/whereareiam/identica/platform/bungeecord/listener/connection/PlayerDisconnectListener.java +++ b/identica-platform/platform-bungeecord/bootstrap/src/main/java/me/whereareiam/identica/platform/bungeecord/listener/connection/PlayerDisconnectListener.java @@ -3,6 +3,7 @@ import com.google.inject.Inject; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; +import me.whereareiam.identica.feature.verification.VerificationService; import me.whereareiam.identica.identity.IdentityAttachment; import me.whereareiam.identica.identity.IdentityService; import me.whereareiam.identica.identity.session.SessionService; @@ -11,7 +12,6 @@ import me.whereareiam.identica.routing.RoutingCoordinator; import me.whereareiam.identica.service.PlatformDeliveryAdapter; import me.whereareiam.identica.type.routing.reason.RoutingClearReason; -import me.whereareiam.identica.verification.VerificationService; import net.md_5.bungee.api.connection.ProxiedPlayer; import net.md_5.bungee.api.event.PlayerDisconnectEvent; diff --git a/identica-platform/platform-velocity/bootstrap/build.gradle.kts b/identica-platform/platform-velocity/bootstrap/build.gradle.kts index e2e850e73..3a9488681 100644 --- a/identica-platform/platform-velocity/bootstrap/build.gradle.kts +++ b/identica-platform/platform-velocity/bootstrap/build.gradle.kts @@ -13,6 +13,7 @@ dependencies { testImplementation(libs.cloud.velocity) implementation(projects.platformVelocityApi) + implementation(projects.featureVerification) implementation(libs.bundles.bStats.velocity) implementation(libs.attache.velocity) diff --git a/identica-platform/platform-velocity/bootstrap/src/main/java/me/whereareiam/identica/platform/velocity/VelocityIdentica.java b/identica-platform/platform-velocity/bootstrap/src/main/java/me/whereareiam/identica/platform/velocity/VelocityIdentica.java index af87ca262..570249550 100644 --- a/identica-platform/platform-velocity/bootstrap/src/main/java/me/whereareiam/identica/platform/velocity/VelocityIdentica.java +++ b/identica-platform/platform-velocity/bootstrap/src/main/java/me/whereareiam/identica/platform/velocity/VelocityIdentica.java @@ -22,6 +22,7 @@ import me.whereareiam.identica.event.lifecycle.IdenticaBootstrappedEvent; import me.whereareiam.identica.event.lifecycle.IdenticaReadyEvent; import me.whereareiam.identica.event.lifecycle.IdenticaShutdownEvent; +import me.whereareiam.identica.feature.verification.VerificationFeatureConfiguration; import me.whereareiam.identica.platform.velocity.logging.VelocityLoggingHelper; import me.whereareiam.identica.type.PluginType; import org.jetbrains.annotations.Nullable; @@ -66,6 +67,7 @@ public void onProxyInitialization(ProxyInitializeEvent event) { Injector injector = Guice.createInjector( new CommonConfiguration(dataPath), + new VerificationFeatureConfiguration(), new EngineConfiguration(), new VelocityConfiguration(proxyServer, this, pluginContainer, dataPath, logger), new CommandConfiguration(), diff --git a/identica-platform/platform-velocity/bootstrap/src/main/java/me/whereareiam/identica/platform/velocity/listener/connection/DisconnectListener.java b/identica-platform/platform-velocity/bootstrap/src/main/java/me/whereareiam/identica/platform/velocity/listener/connection/DisconnectListener.java index 35f045d91..d7abf64ab 100644 --- a/identica-platform/platform-velocity/bootstrap/src/main/java/me/whereareiam/identica/platform/velocity/listener/connection/DisconnectListener.java +++ b/identica-platform/platform-velocity/bootstrap/src/main/java/me/whereareiam/identica/platform/velocity/listener/connection/DisconnectListener.java @@ -5,6 +5,7 @@ import com.velocitypowered.api.event.connection.DisconnectEvent; import com.velocitypowered.api.proxy.Player; import lombok.RequiredArgsConstructor; +import me.whereareiam.identica.feature.verification.VerificationService; import me.whereareiam.identica.identity.IdentityAttachment; import me.whereareiam.identica.identity.IdentityService; import me.whereareiam.identica.identity.session.SessionService; @@ -12,7 +13,6 @@ import me.whereareiam.identica.pipeline.prepare.PrepareStateStore; import me.whereareiam.identica.routing.RoutingCoordinator; import me.whereareiam.identica.type.routing.reason.RoutingClearReason; -import me.whereareiam.identica.verification.VerificationService; @Singleton @RequiredArgsConstructor(onConstructor_ = @Inject) diff --git a/identica-provider/provider-credential/credential-api/src/main/java/me/whereareiam/identica/provider/credential/config/CredentialMessages.java b/identica-provider/provider-credential/credential-api/src/main/java/me/whereareiam/identica/provider/credential/config/CredentialMessages.java index 3e2898390..a8060410b 100644 --- a/identica-provider/provider-credential/credential-api/src/main/java/me/whereareiam/identica/provider/credential/config/CredentialMessages.java +++ b/identica-provider/provider-credential/credential-api/src/main/java/me/whereareiam/identica/provider/credential/config/CredentialMessages.java @@ -7,6 +7,9 @@ import java.util.List; +/** + * Message configuration for the credential provider. + */ @Getter @Setter @ToString @@ -21,7 +24,7 @@ public class CredentialMessages extends ConfigDocument { @Setter @ToString public static class Completion { - private Pipeline session; + private Pipeline recognition; private Pipeline authentication; private Pipeline registration; private Pipeline migration; diff --git a/identica-provider/provider-credential/credential-common/build.gradle.kts b/identica-provider/provider-credential/credential-common/build.gradle.kts index 36d4aea25..48d2db76c 100644 --- a/identica-provider/provider-credential/credential-common/build.gradle.kts +++ b/identica-provider/provider-credential/credential-common/build.gradle.kts @@ -3,6 +3,17 @@ plugins { } dependencies { + // capabilities + compileOnly(projects.capabilityRecognition) + compileOnly(projects.featureVerificationApi) + + // general compileOnly(projects.providerCredentialApi) + + // capabilities + testImplementation(projects.capabilityRecognition) + testImplementation(projects.featureVerificationApi) + + // general testImplementation(projects.providerCredentialApi) } diff --git a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/CommonConfiguration.java b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/CommonConfiguration.java index 8b1b6b92a..81e20dff8 100644 --- a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/CommonConfiguration.java +++ b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/CommonConfiguration.java @@ -16,10 +16,12 @@ import me.whereareiam.identica.provider.credential.config.provider.CredentialSettingsProvider; import me.whereareiam.identica.provider.credential.listener.CredentialAccountClearListener; import me.whereareiam.identica.provider.credential.migration.CredentialMigrationPrecheck; +import me.whereareiam.identica.provider.credential.resolver.CredentialProfileSubjectResolver; import me.whereareiam.identica.provider.credential.sentinel.BruteForceSentinelDefinition; import me.whereareiam.identica.provider.credential.sentinel.BruteForceSentinelLifecycle; import me.whereareiam.identica.provider.credential.util.PasswordRules; import me.whereareiam.identica.provider.migration.ProviderMigrationPrecheck; +import me.whereareiam.identica.provider.profile.ProfileSubjectResolver; public class CommonConfiguration extends AbstractModule { @Override @@ -60,5 +62,8 @@ protected void configure() { Multibinder.newSetBinder(binder(), ProviderMigrationPrecheck.class) .addBinding() .to(CredentialMigrationPrecheck.class); + Multibinder.newSetBinder(binder(), ProfileSubjectResolver.class) + .addBinding() + .to(CredentialProfileSubjectResolver.class); } } diff --git a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/command/CredentialCommand.java b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/command/CredentialCommand.java index 7394a109b..e67857486 100644 --- a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/command/CredentialCommand.java +++ b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/command/CredentialCommand.java @@ -6,7 +6,9 @@ import me.whereareiam.identica.annotation.Argument; import me.whereareiam.identica.annotation.Command; import me.whereareiam.identica.annotation.Definition; -import me.whereareiam.identica.command.ProtectedActionCommand; +import me.whereareiam.identica.feature.verification.VerificationService; +import me.whereareiam.identica.feature.verification.command.ProtectedActionCommand; +import me.whereareiam.identica.feature.verification.config.VerificationMessages; import me.whereareiam.identica.identity.actor.Identity; import me.whereareiam.identica.identity.session.SessionService; import me.whereareiam.identica.model.Session; @@ -16,15 +18,12 @@ import me.whereareiam.identica.model.migration.operation.MigrationConfirm; import me.whereareiam.identica.model.migration.operation.MigrationRequest; import me.whereareiam.identica.model.migration.operation.MigrationResult; -import me.whereareiam.identica.provider.ProviderManager; import me.whereareiam.identica.provider.credential.CredentialConstants; import me.whereareiam.identica.provider.credential.config.CredentialMessages; import me.whereareiam.identica.service.MigrationService; import me.whereareiam.identica.type.migration.MigrationCancelScope; import me.whereareiam.identica.type.migration.MigrationInitiator; import me.whereareiam.identica.type.migration.MigrationResultStatus; -import me.whereareiam.identica.type.provider.ProviderCapability; -import me.whereareiam.identica.verification.VerificationService; import me.whereareiam.keystone.Actor; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -35,24 +34,24 @@ public class CredentialCommand extends ProtectedActionCommand { private final Provider messagesProvider; private final Provider coreMessagesProvider; + private final Provider verificationMessagesProvider; private final MigrationService migrationService; - private final ProviderManager providerManager; private final SessionService sessionService; @Inject public CredentialCommand( Provider messagesProvider, Provider coreMessagesProvider, + Provider verificationMessagesProvider, MigrationService migrationService, - ProviderManager providerManager, VerificationService verificationService, SessionService sessionService ) { super(verificationService); this.messagesProvider = messagesProvider; this.coreMessagesProvider = coreMessagesProvider; + this.verificationMessagesProvider = verificationMessagesProvider; this.migrationService = migrationService; - this.providerManager = providerManager; this.sessionService = sessionService; } @@ -74,7 +73,6 @@ public void credential(@NotNull Actor sender) { Session session = requireCurrentSession(identity); if (session == null) return; - if (!supportsMigration()) return; UUID accountUniqueId = requireAccountUniqueId(identity); if (accountUniqueId == null) return; @@ -100,7 +98,7 @@ public void credential(@NotNull Actor sender) { StepUpPreparation preparation = prepareStepUp(accountUniqueId, "migration-confirm"); if (preparation.getStatus() == StepUpPreparation.Status.SELECTION_REQUIRED) { - sendMessage(identity, coreMessagesProvider.get().getCommands().getVerification().getConfirm().getProtectedActionSelectionRequired()); + sendMessage(identity, verificationMessagesProvider.get().getCommands().getConfirm().getProtectedActionSelectionRequired()); return; } sendMessage(identity, messages.getVerificationRequired()); @@ -117,9 +115,8 @@ public void credential(@NotNull Actor sender) { @Command("credential confirm [input]") public void confirm(@NotNull Actor sender, @Argument("input") @Nullable String input) { Identity identity = requireIdentity(sender, null); - if (identity == null) return; + if (identity == null || requireCurrentSession(identity) == null) return; - if (requireCurrentSession(identity) == null) return; UUID accountUniqueId = requireAccountUniqueId(identity); if (accountUniqueId == null) return; @@ -131,8 +128,8 @@ public void confirm(@NotNull Actor sender, @Argument("input") @Nullable String i StepUpPreparation preparation = prepareStepUp(accountUniqueId, "migration-confirm"); if (preparation.getStatus() != StepUpPreparation.Status.READY) { switch (preparation.getStatus()) { - case CURRENT_SESSION_REQUIRED -> sendMessage(identity, coreMessagesProvider.get().getCommands().getVerification().getConfirm().getProtectedActionSessionRequired()); - case SELECTION_REQUIRED -> sendMessage(identity, coreMessagesProvider.get().getCommands().getVerification().getConfirm().getProtectedActionSelectionRequired()); + case CURRENT_SESSION_REQUIRED -> sendMessage(identity, verificationMessagesProvider.get().getCommands().getConfirm().getProtectedActionSessionRequired()); + case SELECTION_REQUIRED -> sendMessage(identity, verificationMessagesProvider.get().getCommands().getConfirm().getProtectedActionSelectionRequired()); default -> sendMessage(identity, messagesProvider.get().getCommands().getCredential().getNoPending()); } return; @@ -146,9 +143,9 @@ public void confirm(@NotNull Actor sender, @Argument("input") @Nullable String i StepUpResult result = confirmStepUp(accountUniqueId, input, "migration-confirm"); if (result.getStatus() != StepUpResult.Status.VERIFIED) { switch (result.getStatus()) { - case INVALID_CODE -> sendMessage(identity, coreMessagesProvider.get().getCommands().getVerification().getConfirm().getInvalidCode()); - case CURRENT_SESSION_REQUIRED -> sendMessage(identity, coreMessagesProvider.get().getCommands().getVerification().getConfirm().getProtectedActionSessionRequired()); - case SELECTION_REQUIRED -> sendMessage(identity, coreMessagesProvider.get().getCommands().getVerification().getConfirm().getProtectedActionSelectionRequired()); + case INVALID_CODE -> sendMessage(identity, verificationMessagesProvider.get().getCommands().getConfirm().getInvalidCode()); + case CURRENT_SESSION_REQUIRED -> sendMessage(identity, verificationMessagesProvider.get().getCommands().getConfirm().getProtectedActionSessionRequired()); + case SELECTION_REQUIRED -> sendMessage(identity, verificationMessagesProvider.get().getCommands().getConfirm().getProtectedActionSelectionRequired()); default -> sendMessage(identity, messagesProvider.get().getCommands().getCredential().getNoPending()); } return; @@ -184,8 +181,7 @@ public void confirm(@NotNull Actor sender, @Argument("input") @Nullable String i @Command("credential cancel") public void cancel(@NotNull Actor sender) { Identity identity = requireIdentity(sender, null); - if (identity == null) return; - if (requireCurrentSession(identity) == null) return; + if (identity == null || requireCurrentSession(identity) == null) return; var result = migrationService.cancel(MigrationCancel.builder() .connectionUniqueId(identity.getConnectionUniqueId()) @@ -201,13 +197,6 @@ public void cancel(@NotNull Actor sender) { sendMessage(identity, messages.getNoPending()); } - private boolean supportsMigration() { - return providerManager.findProviders(ProviderCapability.MIGRATION).stream() - .anyMatch(provider -> provider != null - && provider.getDescriptor() != null - && CredentialConstants.PROVIDER_ID.equalsIgnoreCase(provider.getDescriptor().getId())); - } - private void sendMessage(@NotNull Identity identity, String message) { if (message == null || message.isBlank()) return; identity.sendMessage(Serializer.serialize(identity, message)); diff --git a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/command/LoginCommand.java b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/command/LoginCommand.java index cc169575a..e96ef5647 100644 --- a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/command/LoginCommand.java +++ b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/command/LoginCommand.java @@ -13,8 +13,8 @@ import me.whereareiam.identica.model.auth.request.AdvanceRequest; import me.whereareiam.identica.model.config.Engine; import me.whereareiam.identica.model.pipeline.journey.JourneyStateItem; -import me.whereareiam.identica.model.pipeline.state.PipelineState; -import me.whereareiam.identica.model.pipeline.state.PipelineStateReference; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.PipelineStateReference; import me.whereareiam.identica.pipeline.state.PipelineStateStore; import me.whereareiam.identica.provider.credential.config.CredentialMessages; import me.whereareiam.identica.provider.credential.pipeline.CredentialAuthenticationAttempt; diff --git a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/command/PassCommand.java b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/command/PassCommand.java index 0b7c9bf89..11ec655bb 100644 --- a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/command/PassCommand.java +++ b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/command/PassCommand.java @@ -13,8 +13,8 @@ import me.whereareiam.identica.model.auth.request.AdvanceRequest; import me.whereareiam.identica.model.config.Engine; import me.whereareiam.identica.model.pipeline.journey.JourneyStateItem; -import me.whereareiam.identica.model.pipeline.state.PipelineState; -import me.whereareiam.identica.model.pipeline.state.PipelineStateReference; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.PipelineStateReference; import me.whereareiam.identica.pipeline.state.PipelineStateStore; import me.whereareiam.identica.provider.credential.config.CredentialMessages; import me.whereareiam.identica.provider.credential.pipeline.CredentialRegistrationAttempt; diff --git a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/completion/CredentialCompletionStep.java b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/completion/CredentialCompletionStep.java index 3495e9356..be46e7b6a 100644 --- a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/completion/CredentialCompletionStep.java +++ b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/completion/CredentialCompletionStep.java @@ -5,21 +5,28 @@ import com.google.inject.Singleton; import me.whereareiam.identica.model.pipeline.completion.CompletionContext; import me.whereareiam.identica.pipeline.completion.step.AbstractMessageCompletionStep; +import me.whereareiam.identica.provider.capability.recognition.store.RecognizedConnectionStore; import me.whereareiam.identica.provider.credential.config.CredentialMessages; import me.whereareiam.identica.type.pipeline.PipelineType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.List; +import java.util.UUID; @Singleton public class CredentialCompletionStep extends AbstractMessageCompletionStep { private final Provider messagesProvider; + private final RecognizedConnectionStore recognizedConnectionStore; @Inject - public CredentialCompletionStep(Provider messagesProvider) { + public CredentialCompletionStep( + Provider messagesProvider, + RecognizedConnectionStore recognizedConnectionStore + ) { super("password-completion"); this.messagesProvider = messagesProvider; + this.recognizedConnectionStore = recognizedConnectionStore; } @Override @@ -49,9 +56,14 @@ public CredentialCompletionStep(Provider messagesProvider) { return completion.getMigration(); if (context.getPipelineType() == PipelineType.REGISTRATION) return completion.getRegistration(); - if (context.isAuthenticationRecognized()) - return completion.getSession(); + if (isRecognized(context)) + return completion.getRecognition(); return completion.getAuthentication(); } + + private boolean isRecognized(@NotNull CompletionContext context) { + UUID connectionUniqueId = context.getIdentity().getConnectionUniqueId(); + return connectionUniqueId != null && recognizedConnectionStore.isRecognized(connectionUniqueId); + } } diff --git a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/config/defaults/CredentialMessagesDefaults.java b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/config/defaults/CredentialMessagesDefaults.java index 67fe7ce76..9520ca09e 100644 --- a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/config/defaults/CredentialMessagesDefaults.java +++ b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/config/defaults/CredentialMessagesDefaults.java @@ -145,12 +145,12 @@ public CredentialMessages supply(CredentialMessages messages) { messages.setScenario(scenario); CredentialMessages.Completion completion = new CredentialMessages.Completion(); - CredentialMessages.Completion.Pipeline sessionCompletion = new CredentialMessages.Completion.Pipeline(); - CredentialMessages.Completion.Pipeline.Title sessionTitle = new CredentialMessages.Completion.Pipeline.Title(); - sessionTitle.setTitle("Session Restored"); - sessionTitle.setSubtitle("Your session was reused."); - sessionCompletion.setTitle(sessionTitle); - sessionCompletion.setBody(List.of( + CredentialMessages.Completion.Pipeline recognitionCompletion = new CredentialMessages.Completion.Pipeline(); + CredentialMessages.Completion.Pipeline.Title recognitionTitle = new CredentialMessages.Completion.Pipeline.Title(); + recognitionTitle.setTitle("Session Restored"); + recognitionTitle.setSubtitle("Your session was reused."); + recognitionCompletion.setTitle(recognitionTitle); + recognitionCompletion.setBody(List.of( " ", " Identica", " ", @@ -160,7 +160,7 @@ public CredentialMessages supply(CredentialMessages messages) { " Enjoy your stay.", " " )); - completion.setSession(sessionCompletion); + completion.setRecognition(recognitionCompletion); CredentialMessages.Completion.Pipeline authenticationCompletion = new CredentialMessages.Completion.Pipeline(); CredentialMessages.Completion.Pipeline.Title authenticationTitle = new CredentialMessages.Completion.Pipeline.Title(); diff --git a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/CredentialPipelineExtension.java b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/CredentialPipelineExtension.java index 92ecb0283..eb7fbb1e6 100644 --- a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/CredentialPipelineExtension.java +++ b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/CredentialPipelineExtension.java @@ -8,17 +8,16 @@ import me.whereareiam.identica.pipeline.journey.step.Step; import me.whereareiam.identica.provider.credential.CredentialConstants; import me.whereareiam.identica.provider.credential.config.CredentialSettings; -import me.whereareiam.identica.provider.credential.pipeline.scenario.CredentialValidationStep; -import me.whereareiam.identica.provider.credential.pipeline.scenario.authentication.CredentialAuthenticationPasswordStep; -import me.whereareiam.identica.provider.credential.pipeline.scenario.authentication.CredentialAuthenticationVerificationStep; -import me.whereareiam.identica.provider.credential.pipeline.scenario.authentication.CredentialRecognitionStep; -import me.whereareiam.identica.provider.credential.pipeline.scenario.migration.CredentialMigrationAuthenticationStep; -import me.whereareiam.identica.provider.credential.pipeline.scenario.migration.CredentialMigrationConfirmStep; -import me.whereareiam.identica.provider.credential.pipeline.scenario.migration.CredentialMigrationRegistrationStep; -import me.whereareiam.identica.provider.credential.pipeline.scenario.registration.CredentialAccountPresenceStep; -import me.whereareiam.identica.provider.credential.pipeline.scenario.registration.CredentialRegistrationConfirmStep; -import me.whereareiam.identica.provider.credential.pipeline.scenario.registration.CredentialRegistrationStep; -import me.whereareiam.identica.type.pipeline.PipelineScope; +import me.whereareiam.identica.provider.credential.pipeline.step.shared.CredentialValidationStep; +import me.whereareiam.identica.provider.credential.pipeline.step.type.authentication.CredentialAuthenticationPasswordStep; +import me.whereareiam.identica.provider.credential.pipeline.step.type.authentication.CredentialAuthenticationVerificationStep; +import me.whereareiam.identica.provider.credential.pipeline.step.type.authentication.CredentialRecognitionStep; +import me.whereareiam.identica.provider.credential.pipeline.step.type.migration.CredentialMigrationAuthenticationStep; +import me.whereareiam.identica.provider.credential.pipeline.step.type.migration.CredentialMigrationConfirmStep; +import me.whereareiam.identica.provider.credential.pipeline.step.type.migration.CredentialMigrationRegistrationStep; +import me.whereareiam.identica.provider.credential.pipeline.step.type.registration.CredentialAccountPresenceStep; +import me.whereareiam.identica.provider.credential.pipeline.step.type.registration.CredentialRegistrationConfirmStep; +import me.whereareiam.identica.provider.credential.pipeline.step.type.registration.CredentialRegistrationStep; import me.whereareiam.identica.type.pipeline.PipelineType; import me.whereareiam.identica.type.pipeline.journey.JourneyMode; import me.whereareiam.identica.type.pipeline.journey.StageType; @@ -71,92 +70,80 @@ public void apply(@NotNull PipelineExtensionBuilder builder) { registerForBothJourneyModes( builder, - PipelineScope.REGISTRATION, - providerId, PipelineType.REGISTRATION, + providerId, validationStep ); registerForBothJourneyModes( builder, - PipelineScope.REGISTRATION, - providerId, PipelineType.REGISTRATION, + providerId, credentialAccountPresenceStep ); registerForBothJourneyModes( builder, - PipelineScope.REGISTRATION, - providerId, PipelineType.REGISTRATION, + providerId, credentialRegistrationStep ); if (requireRepeat) { registerForBothJourneyModes( builder, - PipelineScope.REGISTRATION, - providerId, PipelineType.REGISTRATION, + providerId, credentialRegistrationConfirmStep ); } registerForBothJourneyModes( builder, - PipelineScope.AUTHENTICATION, - providerId, PipelineType.AUTHENTICATION, + providerId, validationStep ); registerForBothJourneyModes( builder, - PipelineScope.AUTHENTICATION, - providerId, PipelineType.AUTHENTICATION, + providerId, recognitionStep ); registerForBothJourneyModes( builder, - PipelineScope.AUTHENTICATION, - providerId, PipelineType.AUTHENTICATION, + providerId, authenticationPasswordStep ); registerForBothJourneyModes( builder, - PipelineScope.AUTHENTICATION, - providerId, PipelineType.AUTHENTICATION, + providerId, authenticationVerificationStep ); builder.registerStep( - PipelineScope.MIGRATION, + PipelineType.MIGRATION, providerId, StageType.PROVIDER, - PipelineType.MIGRATION, validationStep ); builder.registerStep( - PipelineScope.MIGRATION, + PipelineType.MIGRATION, providerId, StageType.PROVIDER, - PipelineType.MIGRATION, migrationAuthenticationStep ); builder.registerStep( - PipelineScope.MIGRATION, + PipelineType.MIGRATION, providerId, StageType.PROVIDER, - PipelineType.MIGRATION, migrationRegistrationStep ); if (requireRepeat) { builder.registerStep( - PipelineScope.MIGRATION, + PipelineType.MIGRATION, providerId, StageType.PROVIDER, - PipelineType.MIGRATION, migrationConfirmStep ); } @@ -164,12 +151,11 @@ public void apply(@NotNull PipelineExtensionBuilder builder) { private void registerForBothJourneyModes( @NotNull PipelineExtensionBuilder builder, - @NotNull PipelineScope scope, - @NotNull String providerId, @NotNull PipelineType pipelineType, + @NotNull String providerId, @NotNull Step step ) { - builder.registerStep(scope, providerId, StageType.PROVIDER, pipelineType, JourneyMode.SEAMLESS, step); - builder.registerStep(scope, providerId, StageType.PROVIDER, pipelineType, JourneyMode.INTERACTIVE, step); + builder.registerStep(pipelineType, providerId, StageType.PROVIDER, JourneyMode.SEAMLESS, step); + builder.registerStep(pipelineType, providerId, StageType.PROVIDER, JourneyMode.INTERACTIVE, step); } } diff --git a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/base/AbstractCredentialAttemptStep.java b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/base/AbstractCredentialAttemptStep.java similarity index 96% rename from identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/base/AbstractCredentialAttemptStep.java rename to identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/base/AbstractCredentialAttemptStep.java index 2db315621..dea36dd41 100644 --- a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/base/AbstractCredentialAttemptStep.java +++ b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/base/AbstractCredentialAttemptStep.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.provider.credential.pipeline.scenario.base; +package me.whereareiam.identica.provider.credential.pipeline.step.base; import com.google.inject.Provider; import me.whereareiam.identica.event.EventManager; diff --git a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/base/AbstractCredentialConfirmStep.java b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/base/AbstractCredentialConfirmStep.java similarity index 98% rename from identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/base/AbstractCredentialConfirmStep.java rename to identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/base/AbstractCredentialConfirmStep.java index 78dd9db5b..40c73ba48 100644 --- a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/base/AbstractCredentialConfirmStep.java +++ b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/base/AbstractCredentialConfirmStep.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.provider.credential.pipeline.scenario.base; +package me.whereareiam.identica.provider.credential.pipeline.step.base; import com.google.inject.Provider; import me.whereareiam.identica.model.config.Engine; diff --git a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/base/AbstractCredentialPasswordStep.java b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/base/AbstractCredentialPasswordStep.java similarity index 97% rename from identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/base/AbstractCredentialPasswordStep.java rename to identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/base/AbstractCredentialPasswordStep.java index 25d8769d9..791d75d44 100644 --- a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/base/AbstractCredentialPasswordStep.java +++ b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/base/AbstractCredentialPasswordStep.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.provider.credential.pipeline.scenario.base; +package me.whereareiam.identica.provider.credential.pipeline.step.base; import com.google.inject.Provider; import me.whereareiam.identica.event.EventManager; diff --git a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/base/AbstractCredentialSetupStep.java b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/base/AbstractCredentialSetupStep.java similarity index 97% rename from identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/base/AbstractCredentialSetupStep.java rename to identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/base/AbstractCredentialSetupStep.java index 83d24e47e..eca6d6e9b 100644 --- a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/base/AbstractCredentialSetupStep.java +++ b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/base/AbstractCredentialSetupStep.java @@ -1,8 +1,7 @@ -package me.whereareiam.identica.provider.credential.pipeline.scenario.base; +package me.whereareiam.identica.provider.credential.pipeline.step.base; import com.google.inject.Provider; import me.whereareiam.identica.model.config.Engine; -import me.whereareiam.identica.model.config.Settings; import me.whereareiam.identica.model.pipeline.journey.stage.step.StepResult; import me.whereareiam.identica.pipeline.ScenarioContext; import me.whereareiam.identica.pipeline.state.PipelineStateStore; diff --git a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/base/AbstractCredentialStatefulStep.java b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/base/AbstractCredentialStatefulStep.java similarity index 95% rename from identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/base/AbstractCredentialStatefulStep.java rename to identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/base/AbstractCredentialStatefulStep.java index 416f4e78d..a289a1b34 100644 --- a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/base/AbstractCredentialStatefulStep.java +++ b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/base/AbstractCredentialStatefulStep.java @@ -1,10 +1,10 @@ -package me.whereareiam.identica.provider.credential.pipeline.scenario.base; +package me.whereareiam.identica.provider.credential.pipeline.step.base; import com.google.inject.Provider; import me.whereareiam.identica.model.config.Engine; -import me.whereareiam.identica.model.pipeline.state.PipelineState; -import me.whereareiam.identica.model.pipeline.state.PipelineStateReference; import me.whereareiam.identica.pipeline.ScenarioContext; +import me.whereareiam.identica.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.PipelineStateReference; import me.whereareiam.identica.pipeline.state.PipelineStateStore; import me.whereareiam.identica.provider.credential.pipeline.CredentialAuthenticationAttempt; import me.whereareiam.identica.provider.credential.pipeline.CredentialRegisterStateItem; diff --git a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/base/AbstractCredentialStep.java b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/base/AbstractCredentialStep.java similarity index 93% rename from identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/base/AbstractCredentialStep.java rename to identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/base/AbstractCredentialStep.java index 352a8f1db..005833847 100644 --- a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/base/AbstractCredentialStep.java +++ b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/base/AbstractCredentialStep.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.provider.credential.pipeline.scenario.base; +package me.whereareiam.identica.provider.credential.pipeline.step.base; import me.whereareiam.identica.model.provider.ProviderContext; import me.whereareiam.identica.pipeline.ScenarioContext; diff --git a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/CredentialValidationStep.java b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/shared/CredentialValidationStep.java similarity index 92% rename from identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/CredentialValidationStep.java rename to identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/shared/CredentialValidationStep.java index be7ace477..149bef0b6 100644 --- a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/CredentialValidationStep.java +++ b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/shared/CredentialValidationStep.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.provider.credential.pipeline.scenario; +package me.whereareiam.identica.provider.credential.pipeline.step.shared; import com.google.inject.Singleton; import me.whereareiam.identica.logging.Logger; @@ -6,7 +6,7 @@ import me.whereareiam.identica.model.provider.ProviderContext; import me.whereareiam.identica.pipeline.ScenarioContext; import me.whereareiam.identica.provider.credential.CredentialConstants; -import me.whereareiam.identica.provider.credential.pipeline.scenario.base.AbstractCredentialStep; +import me.whereareiam.identica.provider.credential.pipeline.step.base.AbstractCredentialStep; import org.jetbrains.annotations.NotNull; import java.util.concurrent.CompletableFuture; diff --git a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/authentication/CredentialAuthenticationPasswordStep.java b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/type/authentication/CredentialAuthenticationPasswordStep.java similarity index 91% rename from identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/authentication/CredentialAuthenticationPasswordStep.java rename to identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/type/authentication/CredentialAuthenticationPasswordStep.java index 8239fc626..a42942612 100644 --- a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/authentication/CredentialAuthenticationPasswordStep.java +++ b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/type/authentication/CredentialAuthenticationPasswordStep.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.provider.credential.pipeline.scenario.authentication; +package me.whereareiam.identica.provider.credential.pipeline.step.type.authentication; import com.google.inject.Inject; import com.google.inject.Provider; @@ -11,7 +11,7 @@ import me.whereareiam.identica.provider.credential.account.CredentialAccountService; import me.whereareiam.identica.provider.credential.config.CredentialMessages; import me.whereareiam.identica.provider.credential.cryptography.CryptographyService; -import me.whereareiam.identica.provider.credential.pipeline.scenario.base.AbstractCredentialPasswordStep; +import me.whereareiam.identica.provider.credential.pipeline.step.base.AbstractCredentialPasswordStep; import org.jetbrains.annotations.NotNull; @Singleton diff --git a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/authentication/CredentialAuthenticationVerificationStep.java b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/type/authentication/CredentialAuthenticationVerificationStep.java similarity index 87% rename from identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/authentication/CredentialAuthenticationVerificationStep.java rename to identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/type/authentication/CredentialAuthenticationVerificationStep.java index 3997f8e98..08b185ab5 100644 --- a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/authentication/CredentialAuthenticationVerificationStep.java +++ b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/type/authentication/CredentialAuthenticationVerificationStep.java @@ -1,20 +1,20 @@ -package me.whereareiam.identica.provider.credential.pipeline.scenario.authentication; +package me.whereareiam.identica.provider.credential.pipeline.step.type.authentication; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import me.whereareiam.identica.database.provider.ProviderLinkPersistenceService; +import me.whereareiam.identica.feature.verification.VerificationService; +import me.whereareiam.identica.feature.verification.model.resolution.VerificationResolutionRequest; +import me.whereareiam.identica.feature.verification.model.resolution.VerificationResolutionResult; +import me.whereareiam.identica.feature.verification.type.status.VerificationResolutionStatus; import me.whereareiam.identica.logging.Logger; import me.whereareiam.identica.model.identity.provider.AccountProviderLink; import me.whereareiam.identica.model.pipeline.journey.stage.step.StepResult; -import me.whereareiam.identica.model.verification.VerificationResolutionRequest; -import me.whereareiam.identica.model.verification.VerificationResolutionResult; import me.whereareiam.identica.pipeline.ScenarioContext; import me.whereareiam.identica.provider.credential.CredentialConstants; import me.whereareiam.identica.provider.credential.config.CredentialMessages; -import me.whereareiam.identica.provider.credential.pipeline.scenario.base.AbstractCredentialStep; -import me.whereareiam.identica.type.verification.VerificationResolutionStatus; -import me.whereareiam.identica.verification.VerificationService; +import me.whereareiam.identica.provider.credential.pipeline.step.base.AbstractCredentialStep; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/authentication/CredentialRecognitionStep.java b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/type/authentication/CredentialRecognitionStep.java similarity index 57% rename from identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/authentication/CredentialRecognitionStep.java rename to identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/type/authentication/CredentialRecognitionStep.java index 5b9eca736..20d151a2b 100644 --- a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/authentication/CredentialRecognitionStep.java +++ b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/type/authentication/CredentialRecognitionStep.java @@ -1,25 +1,30 @@ -package me.whereareiam.identica.provider.credential.pipeline.scenario.authentication; +package me.whereareiam.identica.provider.credential.pipeline.step.type.authentication; import com.google.inject.Inject; import com.google.inject.Singleton; -import me.whereareiam.identica.identity.session.recognition.SessionRecognitionService; import me.whereareiam.identica.model.pipeline.journey.stage.step.StepResult; import me.whereareiam.identica.pipeline.ScenarioContext; -import me.whereareiam.identica.pipeline.journey.step.type.AuthenticationRecognitionStep; +import me.whereareiam.identica.provider.capability.recognition.SessionRecognitionService; +import me.whereareiam.identica.provider.capability.recognition.store.RecognizedConnectionStore; import me.whereareiam.identica.provider.credential.CredentialConstants; -import me.whereareiam.identica.provider.credential.pipeline.scenario.base.AbstractCredentialStep; +import me.whereareiam.identica.provider.credential.pipeline.step.base.AbstractCredentialStep; import org.jetbrains.annotations.NotNull; import java.util.concurrent.CompletableFuture; @Singleton -public class CredentialRecognitionStep extends AbstractCredentialStep implements AuthenticationRecognitionStep { +public class CredentialRecognitionStep extends AbstractCredentialStep { private final SessionRecognitionService sessionRecognitionService; + private final RecognizedConnectionStore recognizedConnectionStore; @Inject - public CredentialRecognitionStep(SessionRecognitionService sessionRecognitionService) { + public CredentialRecognitionStep( + SessionRecognitionService sessionRecognitionService, + RecognizedConnectionStore recognizedConnectionStore + ) { super("password-recognition"); this.sessionRecognitionService = sessionRecognitionService; + this.recognizedConnectionStore = recognizedConnectionStore; } @Override @@ -32,6 +37,9 @@ public CredentialRecognitionStep(SessionRecognitionService sessionRecognitionSer context.getIp(), context.getIdentity().getOrigin() ); + if (recognized && context.getIdentity().getConnectionUniqueId() != null) + recognizedConnectionStore.markRecognized(context.getIdentity().getConnectionUniqueId()); + return CompletableFuture.completedFuture(recognized ? StepResult.complete(context) : StepResult.proceed(context)); diff --git a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/migration/CredentialMigrationAuthenticationStep.java b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/type/migration/CredentialMigrationAuthenticationStep.java similarity index 91% rename from identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/migration/CredentialMigrationAuthenticationStep.java rename to identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/type/migration/CredentialMigrationAuthenticationStep.java index dcf65d6a4..0914657dc 100644 --- a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/migration/CredentialMigrationAuthenticationStep.java +++ b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/type/migration/CredentialMigrationAuthenticationStep.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.provider.credential.pipeline.scenario.migration; +package me.whereareiam.identica.provider.credential.pipeline.step.type.migration; import com.google.inject.Inject; import com.google.inject.Provider; @@ -11,7 +11,7 @@ import me.whereareiam.identica.provider.credential.account.CredentialAccountService; import me.whereareiam.identica.provider.credential.config.CredentialMessages; import me.whereareiam.identica.provider.credential.cryptography.CryptographyService; -import me.whereareiam.identica.provider.credential.pipeline.scenario.base.AbstractCredentialPasswordStep; +import me.whereareiam.identica.provider.credential.pipeline.step.base.AbstractCredentialPasswordStep; import org.jetbrains.annotations.NotNull; @Singleton diff --git a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/migration/CredentialMigrationConfirmStep.java b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/type/migration/CredentialMigrationConfirmStep.java similarity index 90% rename from identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/migration/CredentialMigrationConfirmStep.java rename to identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/type/migration/CredentialMigrationConfirmStep.java index 0e0c22ae2..5fd5fd878 100644 --- a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/migration/CredentialMigrationConfirmStep.java +++ b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/type/migration/CredentialMigrationConfirmStep.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.provider.credential.pipeline.scenario.migration; +package me.whereareiam.identica.provider.credential.pipeline.step.type.migration; import com.google.inject.Inject; import com.google.inject.Provider; @@ -8,7 +8,7 @@ import me.whereareiam.identica.provider.credential.account.CredentialAccountService; import me.whereareiam.identica.provider.credential.config.CredentialMessages; import me.whereareiam.identica.provider.credential.cryptography.CryptographyService; -import me.whereareiam.identica.provider.credential.pipeline.scenario.base.AbstractCredentialConfirmStep; +import me.whereareiam.identica.provider.credential.pipeline.step.base.AbstractCredentialConfirmStep; import org.jetbrains.annotations.NotNull; @Singleton diff --git a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/migration/CredentialMigrationRegistrationStep.java b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/type/migration/CredentialMigrationRegistrationStep.java similarity index 93% rename from identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/migration/CredentialMigrationRegistrationStep.java rename to identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/type/migration/CredentialMigrationRegistrationStep.java index 8b6d2e330..a7d4e0799 100644 --- a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/migration/CredentialMigrationRegistrationStep.java +++ b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/type/migration/CredentialMigrationRegistrationStep.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.provider.credential.pipeline.scenario.migration; +package me.whereareiam.identica.provider.credential.pipeline.step.type.migration; import com.google.inject.Inject; import com.google.inject.Provider; @@ -12,7 +12,7 @@ import me.whereareiam.identica.provider.credential.config.CredentialSettings; import me.whereareiam.identica.provider.credential.cryptography.CryptographyService; import me.whereareiam.identica.provider.credential.model.CredentialAccount; -import me.whereareiam.identica.provider.credential.pipeline.scenario.base.AbstractCredentialSetupStep; +import me.whereareiam.identica.provider.credential.pipeline.step.base.AbstractCredentialSetupStep; import me.whereareiam.identica.provider.credential.util.PasswordRules; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/registration/CredentialAccountPresenceStep.java b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/type/registration/CredentialAccountPresenceStep.java similarity index 95% rename from identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/registration/CredentialAccountPresenceStep.java rename to identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/type/registration/CredentialAccountPresenceStep.java index fa0b02451..7e4d768e7 100644 --- a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/registration/CredentialAccountPresenceStep.java +++ b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/type/registration/CredentialAccountPresenceStep.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.provider.credential.pipeline.scenario.registration; +package me.whereareiam.identica.provider.credential.pipeline.step.type.registration; import com.google.inject.Inject; import com.google.inject.Provider; @@ -13,7 +13,7 @@ import me.whereareiam.identica.provider.credential.cryptography.CryptographyService; import me.whereareiam.identica.provider.credential.event.authentication.AuthenticationAttemptDecision; import me.whereareiam.identica.provider.credential.model.CredentialAccount; -import me.whereareiam.identica.provider.credential.pipeline.scenario.base.AbstractCredentialAttemptStep; +import me.whereareiam.identica.provider.credential.pipeline.step.base.AbstractCredentialAttemptStep; import org.jetbrains.annotations.NotNull; import java.util.concurrent.CompletableFuture; diff --git a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/registration/CredentialRegistrationConfirmStep.java b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/type/registration/CredentialRegistrationConfirmStep.java similarity index 90% rename from identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/registration/CredentialRegistrationConfirmStep.java rename to identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/type/registration/CredentialRegistrationConfirmStep.java index 58557fc7e..2601829bf 100644 --- a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/registration/CredentialRegistrationConfirmStep.java +++ b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/type/registration/CredentialRegistrationConfirmStep.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.provider.credential.pipeline.scenario.registration; +package me.whereareiam.identica.provider.credential.pipeline.step.type.registration; import com.google.inject.Inject; import com.google.inject.Provider; @@ -8,7 +8,7 @@ import me.whereareiam.identica.provider.credential.account.CredentialAccountService; import me.whereareiam.identica.provider.credential.config.CredentialMessages; import me.whereareiam.identica.provider.credential.cryptography.CryptographyService; -import me.whereareiam.identica.provider.credential.pipeline.scenario.base.AbstractCredentialConfirmStep; +import me.whereareiam.identica.provider.credential.pipeline.step.base.AbstractCredentialConfirmStep; import org.jetbrains.annotations.NotNull; @Singleton diff --git a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/registration/CredentialRegistrationStep.java b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/type/registration/CredentialRegistrationStep.java similarity index 92% rename from identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/registration/CredentialRegistrationStep.java rename to identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/type/registration/CredentialRegistrationStep.java index 5a235b50e..a2fafa9ff 100644 --- a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/scenario/registration/CredentialRegistrationStep.java +++ b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/pipeline/step/type/registration/CredentialRegistrationStep.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.provider.credential.pipeline.scenario.registration; +package me.whereareiam.identica.provider.credential.pipeline.step.type.registration; import com.google.inject.Inject; import com.google.inject.Provider; @@ -12,7 +12,7 @@ import me.whereareiam.identica.provider.credential.config.CredentialSettings; import me.whereareiam.identica.provider.credential.cryptography.CryptographyService; import me.whereareiam.identica.provider.credential.model.CredentialAccount; -import me.whereareiam.identica.provider.credential.pipeline.scenario.base.AbstractCredentialSetupStep; +import me.whereareiam.identica.provider.credential.pipeline.step.base.AbstractCredentialSetupStep; import me.whereareiam.identica.provider.credential.util.PasswordRules; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/resolver/CredentialProfileSubjectResolver.java b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/resolver/CredentialProfileSubjectResolver.java new file mode 100644 index 000000000..e7d4b0f17 --- /dev/null +++ b/identica-provider/provider-credential/credential-common/src/main/java/me/whereareiam/identica/provider/credential/resolver/CredentialProfileSubjectResolver.java @@ -0,0 +1,42 @@ +package me.whereareiam.identica.provider.credential.resolver; + +import me.whereareiam.identica.provider.credential.CredentialConstants; +import me.whereareiam.identica.provider.profile.ProfileResolution; +import me.whereareiam.identica.provider.profile.ProfileResolveContext; +import me.whereareiam.identica.provider.profile.ProfileSubjectResolver; +import me.whereareiam.identica.util.UniqueIdGenerator; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.UUID; + +/** + * Resolves the credential-provider subject from the current username using + * the existing offline UUID convention. + */ +public final class CredentialProfileSubjectResolver implements ProfileSubjectResolver { + @Override + public boolean supports(@NotNull ProfileResolveContext context) { + String username = context.getUsername(); + return username != null && !username.isBlank(); + } + + @Override + public @Nullable ProfileResolution resolve(@NotNull ProfileResolveContext context) { + String username = context.getUsername(); + if (username == null || username.isBlank()) return null; + + UUID subject = UniqueIdGenerator.offlinePlayerUniqueId(username); + if (subject == null) return null; + + return ProfileResolution.builder() + .providerId(CredentialConstants.PROVIDER_ID) + .providerSubject(subject.toString()) + .build(); + } + + @Override + public int priority() { + return 10; + } +} diff --git a/identica-provider/provider-credential/credential-common/src/test/java/me/whereareiam/identica/provider/credential/command/CredentialCommandTest.java b/identica-provider/provider-credential/credential-common/src/test/java/me/whereareiam/identica/provider/credential/command/CredentialCommandTest.java index da979a392..8a894edcb 100644 --- a/identica-provider/provider-credential/credential-common/src/test/java/me/whereareiam/identica/provider/credential/command/CredentialCommandTest.java +++ b/identica-provider/provider-credential/credential-common/src/test/java/me/whereareiam/identica/provider/credential/command/CredentialCommandTest.java @@ -1,6 +1,10 @@ package me.whereareiam.identica.provider.credential.command; import me.whereareiam.identica.Serializer; +import me.whereareiam.identica.feature.verification.VerificationService; +import me.whereareiam.identica.feature.verification.config.VerificationMessages; +import me.whereareiam.identica.feature.verification.model.resolution.VerificationResolutionResult; +import me.whereareiam.identica.feature.verification.type.status.VerificationResolutionStatus; import me.whereareiam.identica.identity.actor.Identity; import me.whereareiam.identica.identity.session.SessionService; import me.whereareiam.identica.model.Session; @@ -8,14 +12,10 @@ import me.whereareiam.identica.model.migration.PendingMigration; import me.whereareiam.identica.model.migration.operation.MigrationConfirm; import me.whereareiam.identica.model.migration.operation.MigrationResult; -import me.whereareiam.identica.model.verification.VerificationResolutionResult; -import me.whereareiam.identica.provider.ProviderManager; import me.whereareiam.identica.provider.credential.config.CredentialMessages; import me.whereareiam.identica.provider.credential.config.defaults.CredentialMessagesDefaults; import me.whereareiam.identica.service.MigrationService; import me.whereareiam.identica.type.migration.MigrationResultStatus; -import me.whereareiam.identica.type.verification.VerificationResolutionStatus; -import me.whereareiam.identica.verification.VerificationService; import me.whereareiam.keystone.model.SerializerContent; import me.whereareiam.keystone.model.SerializerOptions; import me.whereareiam.keystone.serializer.SerializerEngine; @@ -71,8 +71,6 @@ static void initializeSerializer() { @Mock private MigrationService migrationService; @Mock - private ProviderManager providerManager; - @Mock private VerificationService verificationService; @Mock private SessionService sessionService; @@ -91,8 +89,8 @@ void confirmWithoutInputRequestsVerificationCodeWhenStepUpIsRequired() { CredentialCommand command = new CredentialCommand( () -> passwordMessages, Messages::new, + this::verificationMessages, migrationService, - providerManager, verificationService, sessionService ); @@ -116,15 +114,15 @@ void confirmWithInputContinuesMigrationAfterSuccessfulStepUp() { when(verificationService.resolveVerification(any())) .thenReturn(VerificationResolutionResult.of(VerificationResolutionStatus.WAITING, "challenge", "totp", true, false)); when(verificationService.submitChallenge(eq(identity.getUniqueId()), eq("credential"), eq("migration-confirm"), any())) - .thenReturn(me.whereareiam.identica.model.verification.challenge.VerificationChallengeResult.verified(null)); + .thenReturn(me.whereareiam.identica.feature.verification.model.challenge.VerificationChallengeResult.verified(null)); when(migrationService.confirm(any(MigrationConfirm.class))) .thenReturn(MigrationResult.builder().status(MigrationResultStatus.STARTED).build()); CredentialCommand command = new CredentialCommand( () -> new CredentialMessagesDefaults().supply(new CredentialMessages()), Messages::new, + this::verificationMessages, migrationService, - providerManager, verificationService, sessionService ); @@ -150,12 +148,24 @@ private PendingMigration pendingMigration() { .build(); } - private me.whereareiam.identica.model.verification.enrollment.VerificationEnrollment mockEnrollment() { - return me.whereareiam.identica.model.verification.enrollment.VerificationEnrollment.builder() + private me.whereareiam.identica.feature.verification.model.enrollment.VerificationEnrollment mockEnrollment() { + return me.whereareiam.identica.feature.verification.model.enrollment.VerificationEnrollment.builder() .methodId("totp") .build(); } + private VerificationMessages verificationMessages() { + VerificationMessages messages = new VerificationMessages(); + VerificationMessages.Commands commands = new VerificationMessages.Commands(); + VerificationMessages.Commands.Confirm confirm = new VerificationMessages.Commands.Confirm(); + confirm.setProtectedActionSelectionRequired("selection-required"); + confirm.setProtectedActionSessionRequired("session-required"); + confirm.setInvalidCode("invalid-code"); + commands.setConfirm(confirm); + messages.setCommands(commands); + return messages; + } + private static final class TestIdentity extends Identity { private final AtomicReference lastMessage = new AtomicReference<>(); diff --git a/identica-provider/provider-credential/credential-common/src/test/java/me/whereareiam/identica/provider/credential/completion/CredentialCompletionStepTest.java b/identica-provider/provider-credential/credential-common/src/test/java/me/whereareiam/identica/provider/credential/completion/CredentialCompletionStepTest.java index 20cd307e3..388e66551 100644 --- a/identica-provider/provider-credential/credential-common/src/test/java/me/whereareiam/identica/provider/credential/completion/CredentialCompletionStepTest.java +++ b/identica-provider/provider-credential/credential-common/src/test/java/me/whereareiam/identica/provider/credential/completion/CredentialCompletionStepTest.java @@ -4,6 +4,7 @@ import me.whereareiam.identica.identity.actor.Identity; import me.whereareiam.identica.model.Session; import me.whereareiam.identica.model.pipeline.completion.CompletionContext; +import me.whereareiam.identica.provider.capability.recognition.store.RecognizedConnectionStore; import me.whereareiam.identica.provider.credential.config.CredentialMessages; import me.whereareiam.identica.provider.credential.config.defaults.CredentialMessagesDefaults; import me.whereareiam.identica.type.pipeline.PipelineType; @@ -19,6 +20,7 @@ import java.util.UUID; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; @DisplayName("Credential Completion Step") class CredentialCompletionStepTest { @@ -26,18 +28,18 @@ class CredentialCompletionStepTest { @Test void authenticationUsesRecognitionMessageWhenAuthenticationRecognized() { CredentialMessages messages = new CredentialMessagesDefaults().supply(new CredentialMessages()); - InspectableCredentialCompletionStep step = new InspectableCredentialCompletionStep(() -> messages); + InspectableCredentialCompletionStep step = new InspectableCredentialCompletionStep(() -> messages, recognizedStore(true)); List lines = step.lines(context(true, PipelineType.AUTHENTICATION)); - assertEquals(messages.getCompletion().getSession().getBody(), lines); + assertEquals(messages.getCompletion().getRecognition().getBody(), lines); } @DisplayName("Uses the authentication completion message for authentication pipelines") @Test void authenticationUsesAuthenticationCompletionMessage() { CredentialMessages messages = new CredentialMessagesDefaults().supply(new CredentialMessages()); - InspectableCredentialCompletionStep step = new InspectableCredentialCompletionStep(() -> messages); + InspectableCredentialCompletionStep step = new InspectableCredentialCompletionStep(() -> messages, recognizedStore(false)); List lines = step.lines(context(false, PipelineType.AUTHENTICATION)); @@ -48,7 +50,7 @@ void authenticationUsesAuthenticationCompletionMessage() { @Test void registrationUsesRegistrationCompletionMessage() { CredentialMessages messages = new CredentialMessagesDefaults().supply(new CredentialMessages()); - InspectableCredentialCompletionStep step = new InspectableCredentialCompletionStep(() -> messages); + InspectableCredentialCompletionStep step = new InspectableCredentialCompletionStep(() -> messages, recognizedStore(false)); List lines = step.lines(context(false, PipelineType.REGISTRATION)); @@ -59,7 +61,7 @@ void registrationUsesRegistrationCompletionMessage() { @Test void migrationUsesMigrationCompletionMessage() { CredentialMessages messages = new CredentialMessagesDefaults().supply(new CredentialMessages()); - InspectableCredentialCompletionStep step = new InspectableCredentialCompletionStep(() -> messages); + InspectableCredentialCompletionStep step = new InspectableCredentialCompletionStep(() -> messages, recognizedStore(true)); List lines = step.lines(context(true, PipelineType.MIGRATION)); @@ -70,7 +72,7 @@ void migrationUsesMigrationCompletionMessage() { @Test void migrationCompletionCopyAnnouncesCredentialMigrationCompletion() { CredentialMessages messages = new CredentialMessagesDefaults().supply(new CredentialMessages()); - InspectableCredentialCompletionStep step = new InspectableCredentialCompletionStep(() -> messages); + InspectableCredentialCompletionStep step = new InspectableCredentialCompletionStep(() -> messages, recognizedStore(true)); List lines = step.lines(context(true, PipelineType.MIGRATION)); @@ -78,9 +80,10 @@ void migrationCompletionCopyAnnouncesCredentialMigrationCompletion() { assertFalse(lines.stream().anyMatch(line -> line.contains("Welcome back"))); } - private CompletionContext context(boolean authenticationRecognized, PipelineType pipelineType) { + private CompletionContext context(boolean recognized, PipelineType pipelineType) { + TestIdentity identity = new TestIdentity(); return CompletionContext.builder() - .identity(new TestIdentity()) + .identity(identity) .pipelineType(pipelineType) .session(Session.builder() .uniqueId(UUID.randomUUID()) @@ -89,13 +92,21 @@ private CompletionContext context(boolean authenticationRecognized, PipelineType .originalUsername("PlayerOne") .effectiveUsername("PlayerOne") .build()) - .authenticationRecognized(authenticationRecognized) .build(); } + private RecognizedConnectionStore recognizedStore(boolean recognized) { + RecognizedConnectionStore store = mock(RecognizedConnectionStore.class); + when(store.isRecognized(any())).thenReturn(recognized); + return store; + } + private static final class InspectableCredentialCompletionStep extends CredentialCompletionStep { - private InspectableCredentialCompletionStep(Provider messagesProvider) { - super(messagesProvider); + private InspectableCredentialCompletionStep( + Provider messagesProvider, + RecognizedConnectionStore recognizedConnectionStore + ) { + super(messagesProvider, recognizedConnectionStore); } private List lines(CompletionContext context) { @@ -105,7 +116,7 @@ private List lines(CompletionContext context) { private static final class TestIdentity extends Identity { private TestIdentity() { - super(UUID.randomUUID(), "PlayerOne"); + super(UUID.randomUUID(), UUID.randomUUID(), "PlayerOne", "127.0.0.1"); } @Override diff --git a/identica-provider/provider-credential/credential-common/src/test/java/me/whereareiam/identica/provider/credential/pipeline/scenario/authentication/CredentialAuthenticationCredentialStepTest.java b/identica-provider/provider-credential/credential-common/src/test/java/me/whereareiam/identica/provider/credential/pipeline/scenario/authentication/CredentialAuthenticationCredentialStepTest.java index ea2416723..ee891ce09 100644 --- a/identica-provider/provider-credential/credential-common/src/test/java/me/whereareiam/identica/provider/credential/pipeline/scenario/authentication/CredentialAuthenticationCredentialStepTest.java +++ b/identica-provider/provider-credential/credential-common/src/test/java/me/whereareiam/identica/provider/credential/pipeline/scenario/authentication/CredentialAuthenticationCredentialStepTest.java @@ -5,7 +5,7 @@ import me.whereareiam.identica.model.auth.AuthContext; import me.whereareiam.identica.model.config.Engine; import me.whereareiam.identica.model.pipeline.journey.stage.step.StepResult; -import me.whereareiam.identica.model.pipeline.state.PipelineState; +import me.whereareiam.identica.pipeline.state.PipelineState; import me.whereareiam.identica.pipeline.state.PipelineStateStore; import me.whereareiam.identica.provider.credential.account.CredentialAccountService; import me.whereareiam.identica.provider.credential.config.CredentialMessages; @@ -13,6 +13,7 @@ import me.whereareiam.identica.provider.credential.cryptography.CryptographyService; import me.whereareiam.identica.provider.credential.model.CredentialAccount; import me.whereareiam.identica.provider.credential.pipeline.CredentialAuthenticationAttempt; +import me.whereareiam.identica.provider.credential.pipeline.step.type.authentication.CredentialAuthenticationPasswordStep; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -70,7 +71,7 @@ void successfulPasswordAuthenticationContinues() { .build(); when(accountService.find("subject")).thenReturn(Optional.of(account)); - when(pipelineStateStore.find(any(me.whereareiam.identica.model.pipeline.state.PipelineStateReference.class))) + when(pipelineStateStore.find(any(me.whereareiam.identica.pipeline.state.PipelineStateReference.class))) .thenReturn(Optional.of(state)); when(cryptographyService.verify(account, "credential")).thenReturn(true); diff --git a/identica-provider/provider-credential/credential-common/src/test/java/me/whereareiam/identica/provider/credential/pipeline/scenario/authentication/CredentialRecognitionStepTest.java b/identica-provider/provider-credential/credential-common/src/test/java/me/whereareiam/identica/provider/credential/pipeline/scenario/authentication/CredentialRecognitionStepTest.java index 2dc7d2da5..284c899a2 100644 --- a/identica-provider/provider-credential/credential-common/src/test/java/me/whereareiam/identica/provider/credential/pipeline/scenario/authentication/CredentialRecognitionStepTest.java +++ b/identica-provider/provider-credential/credential-common/src/test/java/me/whereareiam/identica/provider/credential/pipeline/scenario/authentication/CredentialRecognitionStepTest.java @@ -1,10 +1,12 @@ package me.whereareiam.identica.provider.credential.pipeline.scenario.authentication; import me.whereareiam.identica.identity.actor.ConnectionIdentity; -import me.whereareiam.identica.identity.session.recognition.SessionRecognitionService; import me.whereareiam.identica.model.auth.AuthContext; import me.whereareiam.identica.model.pipeline.journey.stage.step.StepResult; import me.whereareiam.identica.model.provider.ProviderContext; +import me.whereareiam.identica.provider.capability.recognition.SessionRecognitionService; +import me.whereareiam.identica.provider.capability.recognition.store.RecognizedConnectionStore; +import me.whereareiam.identica.provider.credential.pipeline.step.type.authentication.CredentialRecognitionStep; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -18,32 +20,38 @@ class CredentialRecognitionStepTest { @Test void completesWhenRecognitionMatches() { SessionRecognitionService recognitionService = mock(SessionRecognitionService.class); + RecognizedConnectionStore recognizedConnectionStore = mock(RecognizedConnectionStore.class); when(recognitionService.matches(any(), any(), any(), any(), any())).thenReturn(true); - CredentialRecognitionStep step = new CredentialRecognitionStep(recognitionService); + CredentialRecognitionStep step = new CredentialRecognitionStep(recognitionService, recognizedConnectionStore); AuthContext context = context(); StepResult result = step.execute(context).join(); assertEquals(StepResult.StepStatus.COMPLETE, result.getStatus()); verify(recognitionService).matches("credential", "credential-subject", "whereareiam", "127.0.0.1", null); + verify(recognizedConnectionStore).markRecognized(context.getIdentity().getConnectionUniqueId()); } @DisplayName("Continues authentication when shared session recognition does not match") @Test void continuesWhenRecognitionDoesNotMatch() { SessionRecognitionService recognitionService = mock(SessionRecognitionService.class); + RecognizedConnectionStore recognizedConnectionStore = mock(RecognizedConnectionStore.class); when(recognitionService.matches(any(), any(), any(), any(), any())).thenReturn(false); - CredentialRecognitionStep step = new CredentialRecognitionStep(recognitionService); + CredentialRecognitionStep step = new CredentialRecognitionStep(recognitionService, recognizedConnectionStore); AuthContext context = context(); StepResult result = step.execute(context).join(); assertEquals(StepResult.StepStatus.CONTINUE, result.getStatus()); + verify(recognizedConnectionStore, never()).markRecognized(any()); } private AuthContext context() { + ConnectionIdentity identity = new ConnectionIdentity("whereareiam", "127.0.0.1"); + identity.setConnectionUniqueId(java.util.UUID.randomUUID()); return AuthContext.builder() - .identity(new ConnectionIdentity("whereareiam", "127.0.0.1")) + .identity(identity) .provider(ProviderContext.of("credential", "credential-subject", "whereareiam", null)) .build(); } diff --git a/identica-provider/provider-credential/credential-common/src/test/java/me/whereareiam/identica/provider/credential/resolver/CredentialProfileSubjectResolverTest.java b/identica-provider/provider-credential/credential-common/src/test/java/me/whereareiam/identica/provider/credential/resolver/CredentialProfileSubjectResolverTest.java new file mode 100644 index 000000000..7f295d9ed --- /dev/null +++ b/identica-provider/provider-credential/credential-common/src/test/java/me/whereareiam/identica/provider/credential/resolver/CredentialProfileSubjectResolverTest.java @@ -0,0 +1,39 @@ +package me.whereareiam.identica.provider.credential.resolver; + +import me.whereareiam.identica.identity.actor.ConnectionIdentity; +import me.whereareiam.identica.provider.credential.CredentialConstants; +import me.whereareiam.identica.provider.profile.ProfileResolution; +import me.whereareiam.identica.provider.profile.ProfileResolveContext; +import me.whereareiam.identica.util.UniqueIdGenerator; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +@DisplayName("Credential Profile Subject Resolver") +class CredentialProfileSubjectResolverTest { + private final CredentialProfileSubjectResolver resolver = new CredentialProfileSubjectResolver(); + + @DisplayName("Derives the credential provider subject from the username") + @Test + void resolvesCredentialSubjectFromUsername() { + ProfileResolution resolution = resolver.resolve(ProfileResolveContext.builder() + .identity(new ConnectionIdentity("PlayerOne", "127.0.0.1")) + .build()); + + assertNotNull(resolution); + assertEquals(CredentialConstants.PROVIDER_ID, resolution.getProviderId()); + assertEquals(UniqueIdGenerator.offlinePlayerUniqueId("PlayerOne").toString(), resolution.getProviderSubject()); + } + + @DisplayName("Rejects blank usernames") + @Test + void doesNotSupportBlankUsernames() { + assertFalse(resolver.supports(ProfileResolveContext.builder() + .identity(new ConnectionIdentity("", "127.0.0.1")) + .build())); + assertNull(resolver.resolve(ProfileResolveContext.builder() + .identity(new ConnectionIdentity("", "127.0.0.1")) + .build())); + } +} diff --git a/identica-provider/provider-credential/credential-database/src/test/java/me/whereareiam/identica/provider/credential/database/migration/V2RenameCrackedProviderIdTest.java b/identica-provider/provider-credential/credential-database/src/test/java/me/whereareiam/identica/provider/credential/database/migration/V2RenameCrackedProviderIdTest.java index da8ea4c39..477c67480 100644 --- a/identica-provider/provider-credential/credential-database/src/test/java/me/whereareiam/identica/provider/credential/database/migration/V2RenameCrackedProviderIdTest.java +++ b/identica-provider/provider-credential/credential-database/src/test/java/me/whereareiam/identica/provider/credential/database/migration/V2RenameCrackedProviderIdTest.java @@ -73,9 +73,19 @@ REFERENCES identica_provider_credential_accounts(provider_id, provider_subject) ON DELETE CASCADE ) """); + handle.execute(""" + CREATE TABLE identica_username_history ( + unique_id UUID NOT NULL, + provider_id VARCHAR(64), + old_username VARCHAR(64) NOT NULL, + new_username VARCHAR(64) NOT NULL, + source VARCHAR(32) NOT NULL, + changed_at BIGINT NOT NULL + ) + """); handle.execute( - "INSERT INTO identica_accounts (unique_id, username, username_source, created_at, last_seen_at) VALUES (?, ?, ?, ?, ?)", - uniqueId, "PlayerOne", "MANUAL", 100L, 200L + "INSERT INTO identica_accounts (unique_id, username, created_at, last_seen_at) VALUES (?, ?, ?, ?)", + uniqueId, "PlayerOne", 100L, 200L ); handle.execute( "INSERT INTO identica_provider_links (unique_id, provider_id, provider_subject, is_primary, linked_at, last_seen_at) VALUES (?, ?, ?, ?, ?, ?)", diff --git a/identica-provider/provider-credential/credential/build.gradle.kts b/identica-provider/provider-credential/credential/build.gradle.kts index cb5259f1f..f06263e4f 100644 --- a/identica-provider/provider-credential/credential/build.gradle.kts +++ b/identica-provider/provider-credential/credential/build.gradle.kts @@ -5,6 +5,13 @@ plugins { } dependencies { + // capabilities + compileOnly(projects.capabilityRestriction) + compileOnly(projects.capabilityRestrictionJoin) + compileOnly(projects.capabilityRecognition) + compileOnly(projects.featureVerificationApi) + + // general implementation(projects.providerCredentialApi) implementation(projects.providerCredentialCommon) implementation(projects.providerCredentialDatabase) @@ -12,6 +19,13 @@ dependencies { implementation(projects.providerCredentialCryptographyBcrypt) implementation(projects.providerCredentialCryptographyArgon2) + // capabilities + testImplementation(projects.capabilityRestriction) + testImplementation(projects.capabilityRestrictionJoin) + testImplementation(projects.capabilityRecognition) + testImplementation(projects.featureVerificationApi) + + // general testImplementation(projects.providerCredentialApi) } diff --git a/identica-provider/provider-credential/credential/src/main/java/me/whereareiam/identica/provider/credential/CredentialProvider.java b/identica-provider/provider-credential/credential/src/main/java/me/whereareiam/identica/provider/credential/CredentialProvider.java index 1db5290c0..2cb8fd0e8 100644 --- a/identica-provider/provider-credential/credential/src/main/java/me/whereareiam/identica/provider/credential/CredentialProvider.java +++ b/identica-provider/provider-credential/credential/src/main/java/me/whereareiam/identica/provider/credential/CredentialProvider.java @@ -5,12 +5,18 @@ import lombok.AllArgsConstructor; import lombok.NoArgsConstructor; import me.whereareiam.identica.BuildConfig; +import me.whereareiam.identica.Constants; import me.whereareiam.identica.Registry; +import me.whereareiam.identica.feature.verification.VerificationFeature; import me.whereareiam.identica.model.provider.dependency.ProviderLibraries; import me.whereareiam.identica.model.provider.dependency.ProviderLibrary; import me.whereareiam.identica.pipeline.completion.extension.CompletionExtensionRegistry; import me.whereareiam.identica.pipeline.extension.PipelineExtensionRegistry; import me.whereareiam.identica.provider.IdenticaProvider; +import me.whereareiam.identica.provider.capability.bootstrap.ProviderCapabilityBootstrap; +import me.whereareiam.identica.provider.capability.recognition.bootstrap.RecognitionCapabilityBootstrap; +import me.whereareiam.identica.provider.capability.restriction.bootstrap.RestrictionCapabilityBootstrap; +import me.whereareiam.identica.provider.capability.restriction.join.bootstrap.JoinRestrictionCapabilityBootstrap; import me.whereareiam.identica.provider.credential.command.CommandRegistrar; import me.whereareiam.identica.provider.credential.completion.CredentialCompletionExtension; import me.whereareiam.identica.provider.credential.cryptography.CryptographyModule; @@ -20,6 +26,7 @@ import me.whereareiam.identica.provider.credential.pipeline.CredentialPipelineExtension; import me.whereareiam.identica.provider.credential.sentinel.BruteForceSentinelDefinition; import me.whereareiam.identica.sentinel.SentinelDefinition; +import me.whereareiam.identica.type.provider.ProviderFeature; import org.jetbrains.annotations.NotNull; import java.util.List; @@ -47,10 +54,63 @@ public class CredentialProvider extends IdenticaProvider { ); } + @Override + public @NotNull List declaredCapabilities() { + return List.of( + RestrictionCapabilityBootstrap.INSTANCE, + JoinRestrictionCapabilityBootstrap.INSTANCE, + RecognitionCapabilityBootstrap.INSTANCE + ); + } + + @Override + public @NotNull List declaredFeatures() { + return List.of(VerificationFeature.FEATURE); + } + @Override public @NotNull ProviderLibraries libraries() { ProviderLibraries libraries = new ProviderLibraries(); libraries.setLibraries(List.of( + ProviderLibrary.builder() + .groupId("me.whereareiam.identica.capability") + .artifactId("restriction-api") + .version(Constants.VERSION) + .resolveTransitiveDependencies(false) + .loader("shared") + .build(), + ProviderLibrary.builder() + .groupId("me.whereareiam.identica.capability") + .artifactId("restriction") + .version(Constants.VERSION) + .resolveTransitiveDependencies(false) + .build(), + ProviderLibrary.builder() + .groupId("me.whereareiam.identica.capability") + .artifactId("restriction-join-api") + .version(Constants.VERSION) + .resolveTransitiveDependencies(false) + .loader("shared") + .build(), + ProviderLibrary.builder() + .groupId("me.whereareiam.identica.capability") + .artifactId("restriction-join") + .version(Constants.VERSION) + .resolveTransitiveDependencies(false) + .build(), + ProviderLibrary.builder() + .groupId("me.whereareiam.identica.capability") + .artifactId("recognition-api") + .version(Constants.VERSION) + .resolveTransitiveDependencies(false) + .loader("shared") + .build(), + ProviderLibrary.builder() + .groupId("me.whereareiam.identica.capability") + .artifactId("recognition") + .version(Constants.VERSION) + .resolveTransitiveDependencies(false) + .build(), ProviderLibrary.builder() .groupId("at.favre.lib") .artifactId("bcrypt") diff --git a/identica-provider/provider-credential/credential/src/main/resources/provider.json b/identica-provider/provider-credential/credential/src/main/resources/provider.json index ade7cd181..dfe24b3a5 100644 --- a/identica-provider/provider-credential/credential/src/main/resources/provider.json +++ b/identica-provider/provider-credential/credential/src/main/resources/provider.json @@ -5,10 +5,5 @@ "main": "me.whereareiam.identica.provider.credential.CredentialProvider", "authors": ["whereareiam"], "supportedPlatforms": ["VELOCITY", "BUNGEECORD"], - "priority": 50, - "capabilities": [ - "offline", - "migration", - "verification" - ] + "priority": 50 } diff --git a/identica-provider/provider-premium/premium/build.gradle.kts b/identica-provider/provider-premium/premium/build.gradle.kts index 92df5e759..694119cd8 100644 --- a/identica-provider/provider-premium/premium/build.gradle.kts +++ b/identica-provider/provider-premium/premium/build.gradle.kts @@ -5,9 +5,26 @@ plugins { } dependencies { + // capabilities + compileOnly(projects.capabilityRestriction) + compileOnly(projects.capabilityRestrictionJoin) + compileOnly(projects.capabilityRecognition) + compileOnly(projects.capabilityAuthoritativeUsername) + compileOnly(projects.featureVerificationApi) + + // general implementation(projects.providerPremiumApi) implementation(projects.providerPremiumPlatformBungeecord) implementation(projects.providerPremiumPlatformVelocity) + + // capabilities + testImplementation(projects.capabilityRestriction) + testImplementation(projects.capabilityRestrictionJoin) + testImplementation(projects.capabilityRecognition) + testImplementation(projects.capabilityAuthoritativeUsername) + testImplementation(projects.featureVerificationApi) + + // general testImplementation(projects.providerPremiumApi) testImplementation(projects.providerPremiumPlatformBungeecord) testImplementation(projects.providerPremiumPlatformVelocity) diff --git a/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/PremiumProvider.java b/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/PremiumProvider.java index 33bf30d03..65cd7e77d 100644 --- a/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/PremiumProvider.java +++ b/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/PremiumProvider.java @@ -4,17 +4,32 @@ import com.google.inject.Module; import lombok.AllArgsConstructor; import lombok.NoArgsConstructor; +import me.whereareiam.identica.Constants; +import me.whereareiam.identica.feature.verification.VerificationFeature; +import me.whereareiam.identica.model.provider.dependency.ProviderLibraries; +import me.whereareiam.identica.model.provider.dependency.ProviderLibrary; import me.whereareiam.identica.pipeline.completion.extension.CompletionExtensionRegistry; import me.whereareiam.identica.pipeline.extension.PipelineExtensionRegistry; import me.whereareiam.identica.provider.IdenticaProvider; import me.whereareiam.identica.provider.ProviderPlatformExtension; +import me.whereareiam.identica.provider.capability.authoritative.username.bootstrap.AuthoritativeUsernameCapabilityBootstrap; +import me.whereareiam.identica.provider.capability.bootstrap.ProviderCapabilityBootstrap; +import me.whereareiam.identica.provider.capability.recognition.bootstrap.RecognitionCapabilityBootstrap; +import me.whereareiam.identica.provider.capability.restriction.bootstrap.RestrictionCapabilityBootstrap; +import me.whereareiam.identica.provider.capability.restriction.join.bootstrap.JoinRestrictionCapabilityBootstrap; import me.whereareiam.identica.provider.premium.command.CommandRegistrar; import me.whereareiam.identica.provider.premium.completion.PremiumCompletionExtension; import me.whereareiam.identica.provider.premium.completion.PremiumCompletionStep; import me.whereareiam.identica.provider.premium.pipeline.PremiumPipelineExtension; +import me.whereareiam.identica.provider.premium.pipeline.step.shared.FinalizeProfileStep; +import me.whereareiam.identica.provider.premium.pipeline.step.shared.OfflineCheckStep; +import me.whereareiam.identica.provider.premium.pipeline.step.shared.ProfilePresenceStep; +import me.whereareiam.identica.provider.premium.pipeline.step.type.authentication.PremiumRecognitionStep; +import me.whereareiam.identica.provider.premium.pipeline.step.type.authentication.PremiumVerificationStep; +import me.whereareiam.identica.provider.premium.pipeline.step.type.migration.PremiumMigrationCompleteStep; import me.whereareiam.identica.provider.premium.platform.bungeecord.PremiumBungeeCordExtension; import me.whereareiam.identica.provider.premium.platform.velocity.PremiumVelocityExtension; -import me.whereareiam.identica.provider.premium.step.*; +import me.whereareiam.identica.type.provider.ProviderFeature; import org.jetbrains.annotations.NotNull; import java.util.List; @@ -41,6 +56,81 @@ public class PremiumProvider extends IdenticaProvider { return List.of(new PremiumModule()); } + @Override + public @NotNull ProviderLibraries libraries() { + ProviderLibraries libraries = new ProviderLibraries(); + libraries.setLibraries(List.of( + ProviderLibrary.builder() + .groupId("me.whereareiam.identica.capability") + .artifactId("restriction-api") + .version(Constants.VERSION) + .resolveTransitiveDependencies(false) + .loader("shared") + .build(), + ProviderLibrary.builder() + .groupId("me.whereareiam.identica.capability") + .artifactId("restriction") + .version(Constants.VERSION) + .resolveTransitiveDependencies(false) + .build(), + ProviderLibrary.builder() + .groupId("me.whereareiam.identica.capability") + .artifactId("restriction-join-api") + .version(Constants.VERSION) + .resolveTransitiveDependencies(false) + .loader("shared") + .build(), + ProviderLibrary.builder() + .groupId("me.whereareiam.identica.capability") + .artifactId("restriction-join") + .version(Constants.VERSION) + .resolveTransitiveDependencies(false) + .build(), + ProviderLibrary.builder() + .groupId("me.whereareiam.identica.capability") + .artifactId("recognition-api") + .version(Constants.VERSION) + .resolveTransitiveDependencies(false) + .loader("shared") + .build(), + ProviderLibrary.builder() + .groupId("me.whereareiam.identica.capability") + .artifactId("recognition") + .version(Constants.VERSION) + .resolveTransitiveDependencies(false) + .build(), + ProviderLibrary.builder() + .groupId("me.whereareiam.identica.capability") + .artifactId("authoritative-username-api") + .version(Constants.VERSION) + .resolveTransitiveDependencies(false) + .loader("shared") + .build(), + ProviderLibrary.builder() + .groupId("me.whereareiam.identica.capability") + .artifactId("authoritative-username") + .version(Constants.VERSION) + .resolveTransitiveDependencies(false) + .build() + )); + return libraries; + } + + @Override + public @NotNull List declaredCapabilities() { + return List.of( + RestrictionCapabilityBootstrap.INSTANCE, + JoinRestrictionCapabilityBootstrap.INSTANCE, + RecognitionCapabilityBootstrap.INSTANCE, + AuthoritativeUsernameCapabilityBootstrap.INSTANCE + ); + } + + @Override + public @NotNull List declaredFeatures() { + return List.of(VerificationFeature.FEATURE); + } + @Override public @NotNull List> platformExtensions() { return List.of( diff --git a/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/command/PremiumCommand.java b/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/command/PremiumCommand.java index fb7e3c1e3..d997e5cab 100644 --- a/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/command/PremiumCommand.java +++ b/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/command/PremiumCommand.java @@ -6,7 +6,9 @@ import me.whereareiam.identica.annotation.Argument; import me.whereareiam.identica.annotation.Command; import me.whereareiam.identica.annotation.Definition; -import me.whereareiam.identica.command.ProtectedActionCommand; +import me.whereareiam.identica.feature.verification.VerificationService; +import me.whereareiam.identica.feature.verification.command.ProtectedActionCommand; +import me.whereareiam.identica.feature.verification.config.VerificationMessages; import me.whereareiam.identica.identity.actor.Identity; import me.whereareiam.identica.identity.session.SessionService; import me.whereareiam.identica.model.Session; @@ -16,15 +18,12 @@ import me.whereareiam.identica.model.migration.operation.MigrationConfirm; import me.whereareiam.identica.model.migration.operation.MigrationRequest; import me.whereareiam.identica.model.migration.operation.MigrationResult; -import me.whereareiam.identica.provider.ProviderManager; import me.whereareiam.identica.provider.premium.PremiumConstants; import me.whereareiam.identica.provider.premium.config.PremiumMessages; import me.whereareiam.identica.service.MigrationService; import me.whereareiam.identica.type.migration.MigrationCancelScope; import me.whereareiam.identica.type.migration.MigrationInitiator; import me.whereareiam.identica.type.migration.MigrationResultStatus; -import me.whereareiam.identica.type.provider.ProviderCapability; -import me.whereareiam.identica.verification.VerificationService; import me.whereareiam.keystone.Actor; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -34,25 +33,25 @@ public class PremiumCommand extends ProtectedActionCommand { private final MigrationService migrationService; - private final ProviderManager providerManager; private final Provider messagesProvider; private final Provider coreMessagesProvider; + private final Provider verificationMessagesProvider; private final SessionService sessionService; @Inject public PremiumCommand( MigrationService migrationService, - ProviderManager providerManager, Provider messagesProvider, Provider coreMessagesProvider, + Provider verificationMessagesProvider, VerificationService verificationService, SessionService sessionService ) { super(verificationService); this.migrationService = migrationService; - this.providerManager = providerManager; this.messagesProvider = messagesProvider; this.coreMessagesProvider = coreMessagesProvider; + this.verificationMessagesProvider = verificationMessagesProvider; this.sessionService = sessionService; } @@ -73,7 +72,6 @@ public void onCommand(@NotNull Actor sender) { if (identity == null) return; Session session = requireCurrentSession(identity); if (session == null) return; - if (!supportsMigration()) return; UUID accountUniqueId = requireAccountUniqueId(identity); if (accountUniqueId == null) return; @@ -97,7 +95,7 @@ public void onCommand(@NotNull Actor sender) { StepUpPreparation preparation = prepareStepUp(accountUniqueId, "migration-confirm"); if (preparation.getStatus() == StepUpPreparation.Status.SELECTION_REQUIRED) { - sendMessage(identity, coreMessagesProvider.get().getCommands().getVerification().getConfirm().getProtectedActionSelectionRequired()); + sendMessage(identity, verificationMessagesProvider.get().getCommands().getConfirm().getProtectedActionSelectionRequired()); return; } sendMessage(identity, messages.getVerificationRequired()); @@ -133,8 +131,8 @@ public void confirm(@NotNull Actor sender, @Argument("input") @Nullable String i StepUpPreparation preparation = prepareStepUp(accountUniqueId, "migration-confirm"); if (preparation.getStatus() != StepUpPreparation.Status.READY) { switch (preparation.getStatus()) { - case CURRENT_SESSION_REQUIRED -> sendMessage(identity, coreMessagesProvider.get().getCommands().getVerification().getConfirm().getProtectedActionSessionRequired()); - case SELECTION_REQUIRED -> sendMessage(identity, coreMessagesProvider.get().getCommands().getVerification().getConfirm().getProtectedActionSelectionRequired()); + case CURRENT_SESSION_REQUIRED -> sendMessage(identity, verificationMessagesProvider.get().getCommands().getConfirm().getProtectedActionSessionRequired()); + case SELECTION_REQUIRED -> sendMessage(identity, verificationMessagesProvider.get().getCommands().getConfirm().getProtectedActionSelectionRequired()); default -> sendMessage(identity, messagesProvider.get().getCommands().getPremium().getNoPending()); } return; @@ -148,9 +146,9 @@ public void confirm(@NotNull Actor sender, @Argument("input") @Nullable String i StepUpResult result = confirmStepUp(accountUniqueId, input, "migration-confirm"); if (result.getStatus() != StepUpResult.Status.VERIFIED) { switch (result.getStatus()) { - case INVALID_CODE -> sendMessage(identity, coreMessagesProvider.get().getCommands().getVerification().getConfirm().getInvalidCode()); - case CURRENT_SESSION_REQUIRED -> sendMessage(identity, coreMessagesProvider.get().getCommands().getVerification().getConfirm().getProtectedActionSessionRequired()); - case SELECTION_REQUIRED -> sendMessage(identity, coreMessagesProvider.get().getCommands().getVerification().getConfirm().getProtectedActionSelectionRequired()); + case INVALID_CODE -> sendMessage(identity, verificationMessagesProvider.get().getCommands().getConfirm().getInvalidCode()); + case CURRENT_SESSION_REQUIRED -> sendMessage(identity, verificationMessagesProvider.get().getCommands().getConfirm().getProtectedActionSessionRequired()); + case SELECTION_REQUIRED -> sendMessage(identity, verificationMessagesProvider.get().getCommands().getConfirm().getProtectedActionSelectionRequired()); default -> sendMessage(identity, messagesProvider.get().getCommands().getPremium().getNoPending()); } return; @@ -200,13 +198,6 @@ public void cancel(@NotNull Actor sender) { sendMessage(identity, messages.getNoPending()); } - private boolean supportsMigration() { - return providerManager.findProviders(ProviderCapability.MIGRATION).stream() - .anyMatch(provider -> provider != null - && provider.getDescriptor() != null - && PremiumConstants.PROVIDER_ID.equalsIgnoreCase(provider.getDescriptor().getId())); - } - private List resolveKickMessage() { PremiumMessages.Commands.Premium messages = messagesProvider.get().getCommands().getPremium(); return messages.getConfirmed(); diff --git a/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/completion/PremiumCompletionStep.java b/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/completion/PremiumCompletionStep.java index 52e8c5f74..033c67edb 100644 --- a/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/completion/PremiumCompletionStep.java +++ b/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/completion/PremiumCompletionStep.java @@ -5,21 +5,28 @@ import com.google.inject.Singleton; import me.whereareiam.identica.model.pipeline.completion.CompletionContext; import me.whereareiam.identica.pipeline.completion.step.AbstractMessageCompletionStep; +import me.whereareiam.identica.provider.capability.recognition.store.RecognizedConnectionStore; import me.whereareiam.identica.provider.premium.config.PremiumMessages; import me.whereareiam.identica.type.pipeline.PipelineType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.List; +import java.util.UUID; @Singleton public class PremiumCompletionStep extends AbstractMessageCompletionStep { private final Provider messagesProvider; + private final RecognizedConnectionStore recognizedConnectionStore; @Inject - public PremiumCompletionStep(Provider messagesProvider) { + public PremiumCompletionStep( + Provider messagesProvider, + RecognizedConnectionStore recognizedConnectionStore + ) { super("premium-completion"); this.messagesProvider = messagesProvider; + this.recognizedConnectionStore = recognizedConnectionStore; } @Override @@ -42,13 +49,15 @@ public PremiumCompletionStep(Provider messagesProvider) { private PremiumMessages.Completion.@NotNull Pipeline resolve(@NotNull CompletionContext context) { PremiumMessages.Completion completion = messagesProvider.get().getCompletion(); - if (context.getPipelineType() == PipelineType.MIGRATION) - return completion.getMigration(); - if (context.getPipelineType() == PipelineType.REGISTRATION) - return completion.getRegistration(); - if (context.isAuthenticationRecognized()) - return completion.getSession(); + if (context.getPipelineType() == PipelineType.MIGRATION) return completion.getMigration(); + if (context.getPipelineType() == PipelineType.REGISTRATION) return completion.getRegistration(); + if (isRecognized(context)) return completion.getRecognition(); return completion.getAuthentication(); } + + private boolean isRecognized(@NotNull CompletionContext context) { + UUID connectionUniqueId = context.getIdentity().getConnectionUniqueId(); + return connectionUniqueId != null && recognizedConnectionStore.isRecognized(connectionUniqueId); + } } diff --git a/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/config/PremiumMessages.java b/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/config/PremiumMessages.java index 86b102000..11361ab8d 100644 --- a/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/config/PremiumMessages.java +++ b/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/config/PremiumMessages.java @@ -20,7 +20,7 @@ public class PremiumMessages extends ConfigDocument { @Setter @ToString public static class Completion { - private @NotNull Pipeline session; + private @NotNull Pipeline recognition; private @NotNull Pipeline authentication; private @NotNull Pipeline registration; private @NotNull Pipeline migration; diff --git a/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/config/defaults/PremiumMessagesDefaults.java b/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/config/defaults/PremiumMessagesDefaults.java index e734bcf6b..60533d2dc 100644 --- a/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/config/defaults/PremiumMessagesDefaults.java +++ b/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/config/defaults/PremiumMessagesDefaults.java @@ -37,12 +37,12 @@ public PremiumMessages supply(PremiumMessages messages) { messages.setVerification(verification); PremiumMessages.Completion completion = new PremiumMessages.Completion(); - PremiumMessages.Completion.Pipeline sessionCompletion = new PremiumMessages.Completion.Pipeline(); - PremiumMessages.Completion.Pipeline.Title sessionTitle = new PremiumMessages.Completion.Pipeline.Title(); - sessionTitle.setTitle("Session Restored"); - sessionTitle.setSubtitle("Your premium session was reused."); - sessionCompletion.setTitle(sessionTitle); - sessionCompletion.setBody(List.of( + PremiumMessages.Completion.Pipeline recognitionCompletion = new PremiumMessages.Completion.Pipeline(); + PremiumMessages.Completion.Pipeline.Title recognitionTitle = new PremiumMessages.Completion.Pipeline.Title(); + recognitionTitle.setTitle("Session Restored"); + recognitionTitle.setSubtitle("Your premium session was reused."); + recognitionCompletion.setTitle(recognitionTitle); + recognitionCompletion.setBody(List.of( " ", " Identica", " ", @@ -52,7 +52,7 @@ public PremiumMessages supply(PremiumMessages messages) { " No new verification was required.", " " )); - completion.setSession(sessionCompletion); + completion.setRecognition(recognitionCompletion); PremiumMessages.Completion.Pipeline authenticationCompletion = new PremiumMessages.Completion.Pipeline(); PremiumMessages.Completion.Pipeline.Title authenticationTitle = new PremiumMessages.Completion.Pipeline.Title(); diff --git a/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/pipeline/PremiumPipelineExtension.java b/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/pipeline/PremiumPipelineExtension.java index 55ce5dd02..9c417074b 100644 --- a/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/pipeline/PremiumPipelineExtension.java +++ b/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/pipeline/PremiumPipelineExtension.java @@ -3,8 +3,12 @@ import lombok.RequiredArgsConstructor; import me.whereareiam.identica.pipeline.extension.PipelineExtension; import me.whereareiam.identica.pipeline.extension.PipelineExtensionBuilder; -import me.whereareiam.identica.provider.premium.step.*; -import me.whereareiam.identica.type.pipeline.PipelineScope; +import me.whereareiam.identica.provider.premium.pipeline.step.shared.FinalizeProfileStep; +import me.whereareiam.identica.provider.premium.pipeline.step.shared.OfflineCheckStep; +import me.whereareiam.identica.provider.premium.pipeline.step.shared.ProfilePresenceStep; +import me.whereareiam.identica.provider.premium.pipeline.step.type.authentication.PremiumRecognitionStep; +import me.whereareiam.identica.provider.premium.pipeline.step.type.authentication.PremiumVerificationStep; +import me.whereareiam.identica.provider.premium.pipeline.step.type.migration.PremiumMigrationCompleteStep; import me.whereareiam.identica.type.pipeline.PipelineType; import me.whereareiam.identica.type.pipeline.journey.StageType; import org.jetbrains.annotations.NotNull; @@ -53,46 +57,40 @@ public void apply(@NotNull PipelineExtensionBuilder builder) { finalizeProfileStep ); builder.registerStep( - PipelineScope.AUTHENTICATION, + PipelineType.AUTHENTICATION, providerId, StageType.PROVIDER, - PipelineType.AUTHENTICATION, premiumRecognitionStep ); builder.registerStep( - PipelineScope.AUTHENTICATION, + PipelineType.AUTHENTICATION, providerId, StageType.PROVIDER, - PipelineType.AUTHENTICATION, premiumVerificationStep ); builder.registerStep( - PipelineScope.MIGRATION, + PipelineType.MIGRATION, providerId, StageType.PROVIDER, - PipelineType.MIGRATION, profilePresenceStep ); builder.registerStep( - PipelineScope.MIGRATION, + PipelineType.MIGRATION, providerId, StageType.PROVIDER, - PipelineType.MIGRATION, offlineCheckStep ); builder.registerStep( - PipelineScope.MIGRATION, + PipelineType.MIGRATION, providerId, StageType.PROVIDER, - PipelineType.MIGRATION, finalizeProfileStep ); builder.registerStep( - PipelineScope.MIGRATION, + PipelineType.MIGRATION, providerId, StageType.PROVIDER, - PipelineType.MIGRATION, premiumMigrationCompleteStep ); } diff --git a/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/step/AbstractProfileVerificationStep.java b/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/pipeline/step/base/AbstractProfileVerificationStep.java similarity index 95% rename from identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/step/AbstractProfileVerificationStep.java rename to identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/pipeline/step/base/AbstractProfileVerificationStep.java index bb50e0c89..f2733a70d 100644 --- a/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/step/AbstractProfileVerificationStep.java +++ b/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/pipeline/step/base/AbstractProfileVerificationStep.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.provider.premium.step; +package me.whereareiam.identica.provider.premium.pipeline.step.base; import com.google.inject.Provider; import me.whereareiam.identica.handshake.HandshakeStore; @@ -6,8 +6,8 @@ import me.whereareiam.identica.model.pipeline.journey.stage.step.StepResult; import me.whereareiam.identica.pipeline.ScenarioContext; import me.whereareiam.identica.pipeline.journey.step.type.SeamlessStep; +import me.whereareiam.identica.pipeline.state.PipelineStateReference; import me.whereareiam.identica.pipeline.state.PipelineStateStore; -import me.whereareiam.identica.model.pipeline.state.PipelineStateReference; import me.whereareiam.identica.provider.premium.PremiumVerifyAttemptItem; import me.whereareiam.identica.provider.premium.config.PremiumMessages; import me.whereareiam.identica.provider.premium.profile.PremiumProfileSnapshot; diff --git a/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/step/FinalizeProfileStep.java b/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/pipeline/step/shared/FinalizeProfileStep.java similarity index 93% rename from identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/step/FinalizeProfileStep.java rename to identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/pipeline/step/shared/FinalizeProfileStep.java index 74072bb1f..d53c49afa 100644 --- a/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/step/FinalizeProfileStep.java +++ b/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/pipeline/step/shared/FinalizeProfileStep.java @@ -1,18 +1,19 @@ -package me.whereareiam.identica.provider.premium.step; +package me.whereareiam.identica.provider.premium.pipeline.step.shared; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import me.whereareiam.identica.handshake.HandshakeStore; import me.whereareiam.identica.logging.Logger; -import me.whereareiam.identica.model.registration.RegistrationContext; import me.whereareiam.identica.model.config.Engine; import me.whereareiam.identica.model.pipeline.journey.stage.step.StepResult; import me.whereareiam.identica.model.provider.ProviderContext; +import me.whereareiam.identica.model.registration.RegistrationContext; import me.whereareiam.identica.pipeline.ScenarioContext; import me.whereareiam.identica.pipeline.state.PipelineStateStore; import me.whereareiam.identica.provider.premium.PremiumConstants; import me.whereareiam.identica.provider.premium.config.PremiumMessages; +import me.whereareiam.identica.provider.premium.pipeline.step.base.AbstractProfileVerificationStep; import me.whereareiam.identica.provider.premium.profile.PremiumProfileStore; import org.jetbrains.annotations.NotNull; diff --git a/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/step/OfflineCheckStep.java b/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/pipeline/step/shared/OfflineCheckStep.java similarity index 96% rename from identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/step/OfflineCheckStep.java rename to identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/pipeline/step/shared/OfflineCheckStep.java index f8399f621..ec8e49031 100644 --- a/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/step/OfflineCheckStep.java +++ b/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/pipeline/step/shared/OfflineCheckStep.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.provider.premium.step; +package me.whereareiam.identica.provider.premium.pipeline.step.shared; import com.google.inject.Inject; import com.google.inject.Provider; @@ -15,6 +15,7 @@ import me.whereareiam.identica.provider.premium.PremiumConstants; import me.whereareiam.identica.provider.premium.config.PremiumMessages; import me.whereareiam.identica.provider.premium.handshake.PremiumHandshakeAttributes; +import me.whereareiam.identica.provider.premium.pipeline.step.base.AbstractProfileVerificationStep; import me.whereareiam.identica.provider.premium.profile.PremiumProfileStore; import me.whereareiam.identica.util.UniqueIdGenerator; import org.jetbrains.annotations.NotNull; diff --git a/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/step/ProfilePresenceStep.java b/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/pipeline/step/shared/ProfilePresenceStep.java similarity index 92% rename from identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/step/ProfilePresenceStep.java rename to identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/pipeline/step/shared/ProfilePresenceStep.java index 96851d6b2..c8a76736a 100644 --- a/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/step/ProfilePresenceStep.java +++ b/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/pipeline/step/shared/ProfilePresenceStep.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.provider.premium.step; +package me.whereareiam.identica.provider.premium.pipeline.step.shared; import com.google.inject.Inject; import com.google.inject.Provider; @@ -10,6 +10,7 @@ import me.whereareiam.identica.pipeline.ScenarioContext; import me.whereareiam.identica.pipeline.state.PipelineStateStore; import me.whereareiam.identica.provider.premium.config.PremiumMessages; +import me.whereareiam.identica.provider.premium.pipeline.step.base.AbstractProfileVerificationStep; import me.whereareiam.identica.provider.premium.profile.PremiumProfileStore; import org.jetbrains.annotations.NotNull; diff --git a/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/step/PremiumRecognitionStep.java b/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/pipeline/step/type/authentication/PremiumRecognitionStep.java similarity index 62% rename from identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/step/PremiumRecognitionStep.java rename to identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/pipeline/step/type/authentication/PremiumRecognitionStep.java index 45ef5f523..52b54df5f 100644 --- a/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/step/PremiumRecognitionStep.java +++ b/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/pipeline/step/type/authentication/PremiumRecognitionStep.java @@ -1,25 +1,30 @@ -package me.whereareiam.identica.provider.premium.step; +package me.whereareiam.identica.provider.premium.pipeline.step.type.authentication; import com.google.inject.Inject; import com.google.inject.Singleton; -import me.whereareiam.identica.identity.session.recognition.SessionRecognitionService; import me.whereareiam.identica.model.pipeline.journey.stage.step.StepResult; import me.whereareiam.identica.model.provider.ProviderContext; import me.whereareiam.identica.pipeline.ScenarioContext; -import me.whereareiam.identica.pipeline.journey.step.type.AuthenticationRecognitionStep; import me.whereareiam.identica.pipeline.journey.step.type.InteractiveStep; +import me.whereareiam.identica.provider.capability.recognition.SessionRecognitionService; +import me.whereareiam.identica.provider.capability.recognition.store.RecognizedConnectionStore; import org.jetbrains.annotations.NotNull; import java.util.concurrent.CompletableFuture; @Singleton -public class PremiumRecognitionStep extends InteractiveStep implements AuthenticationRecognitionStep { +public class PremiumRecognitionStep extends InteractiveStep { private final SessionRecognitionService sessionRecognitionService; + private final RecognizedConnectionStore recognizedConnectionStore; @Inject - public PremiumRecognitionStep(SessionRecognitionService sessionRecognitionService) { + public PremiumRecognitionStep( + SessionRecognitionService sessionRecognitionService, + RecognizedConnectionStore recognizedConnectionStore + ) { super("premium-recognition"); this.sessionRecognitionService = sessionRecognitionService; + this.recognizedConnectionStore = recognizedConnectionStore; } @Override @@ -40,6 +45,9 @@ public int order() { context.getIp(), context.getIdentity().getOrigin() ); + if (recognized && context.getIdentity().getConnectionUniqueId() != null) + recognizedConnectionStore.markRecognized(context.getIdentity().getConnectionUniqueId()); + return CompletableFuture.completedFuture(recognized ? StepResult.complete(context) : StepResult.proceed(context)); diff --git a/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/step/PremiumVerificationStep.java b/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/pipeline/step/type/authentication/PremiumVerificationStep.java similarity index 90% rename from identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/step/PremiumVerificationStep.java rename to identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/pipeline/step/type/authentication/PremiumVerificationStep.java index 3b2c6426b..43929631a 100644 --- a/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/step/PremiumVerificationStep.java +++ b/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/pipeline/step/type/authentication/PremiumVerificationStep.java @@ -1,20 +1,20 @@ -package me.whereareiam.identica.provider.premium.step; +package me.whereareiam.identica.provider.premium.pipeline.step.type.authentication; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import me.whereareiam.identica.database.provider.ProviderLinkPersistenceService; +import me.whereareiam.identica.feature.verification.VerificationService; +import me.whereareiam.identica.feature.verification.model.resolution.VerificationResolutionRequest; +import me.whereareiam.identica.feature.verification.model.resolution.VerificationResolutionResult; +import me.whereareiam.identica.feature.verification.type.status.VerificationResolutionStatus; import me.whereareiam.identica.logging.Logger; import me.whereareiam.identica.model.identity.provider.AccountProviderLink; import me.whereareiam.identica.model.pipeline.journey.stage.step.StepResult; import me.whereareiam.identica.model.provider.ProviderContext; -import me.whereareiam.identica.model.verification.VerificationResolutionRequest; -import me.whereareiam.identica.model.verification.VerificationResolutionResult; import me.whereareiam.identica.pipeline.ScenarioContext; import me.whereareiam.identica.pipeline.journey.step.type.InteractiveStep; import me.whereareiam.identica.provider.premium.config.PremiumMessages; -import me.whereareiam.identica.type.verification.VerificationResolutionStatus; -import me.whereareiam.identica.verification.VerificationService; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/step/PremiumMigrationCompleteStep.java b/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/pipeline/step/type/migration/PremiumMigrationCompleteStep.java similarity index 90% rename from identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/step/PremiumMigrationCompleteStep.java rename to identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/pipeline/step/type/migration/PremiumMigrationCompleteStep.java index b933bb67e..e8fbd1abd 100644 --- a/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/step/PremiumMigrationCompleteStep.java +++ b/identica-provider/provider-premium/premium/src/main/java/me/whereareiam/identica/provider/premium/pipeline/step/type/migration/PremiumMigrationCompleteStep.java @@ -1,4 +1,4 @@ -package me.whereareiam.identica.provider.premium.step; +package me.whereareiam.identica.provider.premium.pipeline.step.type.migration; import com.google.inject.Singleton; import me.whereareiam.identica.model.pipeline.journey.stage.step.StepResult; diff --git a/identica-provider/provider-premium/premium/src/main/resources/provider.json b/identica-provider/provider-premium/premium/src/main/resources/provider.json index ef922b7c1..316ed74d0 100644 --- a/identica-provider/provider-premium/premium/src/main/resources/provider.json +++ b/identica-provider/provider-premium/premium/src/main/resources/provider.json @@ -5,11 +5,5 @@ "main": "me.whereareiam.identica.provider.premium.PremiumProvider", "authors": ["whereareiam"], "supportedPlatforms": ["VELOCITY", "BUNGEECORD"], - "priority": 100, - "capabilities": [ - "online", - "authoritative_username", - "migration", - "verification" - ] + "priority": 100 } diff --git a/identica-provider/provider-premium/premium/src/test/java/me/whereareiam/identica/provider/premium/command/PremiumCommandTest.java b/identica-provider/provider-premium/premium/src/test/java/me/whereareiam/identica/provider/premium/command/PremiumCommandTest.java index ca8316e8b..5a8652577 100644 --- a/identica-provider/provider-premium/premium/src/test/java/me/whereareiam/identica/provider/premium/command/PremiumCommandTest.java +++ b/identica-provider/provider-premium/premium/src/test/java/me/whereareiam/identica/provider/premium/command/PremiumCommandTest.java @@ -1,6 +1,10 @@ package me.whereareiam.identica.provider.premium.command; import me.whereareiam.identica.Serializer; +import me.whereareiam.identica.feature.verification.VerificationService; +import me.whereareiam.identica.feature.verification.config.VerificationMessages; +import me.whereareiam.identica.feature.verification.model.resolution.VerificationResolutionResult; +import me.whereareiam.identica.feature.verification.type.status.VerificationResolutionStatus; import me.whereareiam.identica.identity.actor.Identity; import me.whereareiam.identica.identity.session.SessionService; import me.whereareiam.identica.model.Session; @@ -8,14 +12,10 @@ import me.whereareiam.identica.model.migration.PendingMigration; import me.whereareiam.identica.model.migration.operation.MigrationConfirm; import me.whereareiam.identica.model.migration.operation.MigrationResult; -import me.whereareiam.identica.model.verification.VerificationResolutionResult; -import me.whereareiam.identica.provider.ProviderManager; import me.whereareiam.identica.provider.premium.config.PremiumMessages; import me.whereareiam.identica.provider.premium.config.defaults.PremiumMessagesDefaults; import me.whereareiam.identica.service.MigrationService; import me.whereareiam.identica.type.migration.MigrationResultStatus; -import me.whereareiam.identica.type.verification.VerificationResolutionStatus; -import me.whereareiam.identica.verification.VerificationService; import me.whereareiam.keystone.model.SerializerContent; import me.whereareiam.keystone.model.SerializerOptions; import me.whereareiam.keystone.serializer.SerializerEngine; @@ -71,8 +71,6 @@ static void initializeSerializer() { @Mock private MigrationService migrationService; @Mock - private ProviderManager providerManager; - @Mock private VerificationService verificationService; @Mock private SessionService sessionService; @@ -90,9 +88,9 @@ void confirmWithoutInputRequestsVerificationCodeWhenStepUpIsRequired() { PremiumMessages premiumMessages = new PremiumMessagesDefaults().supply(new PremiumMessages()); PremiumCommand command = new PremiumCommand( migrationService, - providerManager, () -> premiumMessages, Messages::new, + this::verificationMessages, verificationService, sessionService ); @@ -116,15 +114,15 @@ void confirmWithInputContinuesMigrationAfterSuccessfulStepUp() { when(verificationService.resolveVerification(any())) .thenReturn(VerificationResolutionResult.of(VerificationResolutionStatus.WAITING, "challenge", "totp", true, false)); when(verificationService.submitChallenge(eq(identity.getUniqueId()), eq("credential"), eq("migration-confirm"), any())) - .thenReturn(me.whereareiam.identica.model.verification.challenge.VerificationChallengeResult.verified(null)); + .thenReturn(me.whereareiam.identica.feature.verification.model.challenge.VerificationChallengeResult.verified(null)); when(migrationService.confirm(any(MigrationConfirm.class))) .thenReturn(MigrationResult.builder().status(MigrationResultStatus.STARTED).build()); PremiumCommand command = new PremiumCommand( migrationService, - providerManager, () -> new PremiumMessagesDefaults().supply(new PremiumMessages()), Messages::new, + this::verificationMessages, verificationService, sessionService ); @@ -150,12 +148,24 @@ private PendingMigration pendingMigration() { .build(); } - private me.whereareiam.identica.model.verification.enrollment.VerificationEnrollment mockEnrollment() { - return me.whereareiam.identica.model.verification.enrollment.VerificationEnrollment.builder() + private me.whereareiam.identica.feature.verification.model.enrollment.VerificationEnrollment mockEnrollment() { + return me.whereareiam.identica.feature.verification.model.enrollment.VerificationEnrollment.builder() .methodId("totp") .build(); } + private VerificationMessages verificationMessages() { + VerificationMessages messages = new VerificationMessages(); + VerificationMessages.Commands commands = new VerificationMessages.Commands(); + VerificationMessages.Commands.Confirm confirm = new VerificationMessages.Commands.Confirm(); + confirm.setProtectedActionSelectionRequired("selection-required"); + confirm.setProtectedActionSessionRequired("session-required"); + confirm.setInvalidCode("invalid-code"); + commands.setConfirm(confirm); + messages.setCommands(commands); + return messages; + } + private static final class TestIdentity extends Identity { private final AtomicReference lastMessage = new AtomicReference<>(); diff --git a/identica-provider/provider-premium/premium/src/test/java/me/whereareiam/identica/provider/premium/completion/PremiumCompletionStepTest.java b/identica-provider/provider-premium/premium/src/test/java/me/whereareiam/identica/provider/premium/completion/PremiumCompletionStepTest.java index 1b04042e4..460f81525 100644 --- a/identica-provider/provider-premium/premium/src/test/java/me/whereareiam/identica/provider/premium/completion/PremiumCompletionStepTest.java +++ b/identica-provider/provider-premium/premium/src/test/java/me/whereareiam/identica/provider/premium/completion/PremiumCompletionStepTest.java @@ -4,6 +4,7 @@ import me.whereareiam.identica.identity.actor.Identity; import me.whereareiam.identica.model.Session; import me.whereareiam.identica.model.pipeline.completion.CompletionContext; +import me.whereareiam.identica.provider.capability.recognition.store.RecognizedConnectionStore; import me.whereareiam.identica.provider.premium.config.PremiumMessages; import me.whereareiam.identica.provider.premium.config.defaults.PremiumMessagesDefaults; import me.whereareiam.identica.type.pipeline.PipelineType; @@ -19,6 +20,7 @@ import java.util.UUID; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; @DisplayName("Premium Completion Step") class PremiumCompletionStepTest { @@ -26,18 +28,18 @@ class PremiumCompletionStepTest { @Test void authenticationUsesRecognitionMessageWhenAuthenticationRecognized() { PremiumMessages messages = new PremiumMessagesDefaults().supply(new PremiumMessages()); - InspectablePremiumCompletionStep step = new InspectablePremiumCompletionStep(() -> messages); + InspectablePremiumCompletionStep step = new InspectablePremiumCompletionStep(() -> messages, recognizedStore(true)); List lines = step.lines(context(true, PipelineType.AUTHENTICATION)); - assertEquals(messages.getCompletion().getSession().getBody(), lines); + assertEquals(messages.getCompletion().getRecognition().getBody(), lines); } @DisplayName("Keeps the migration completion message even when recognition was applied") @Test void migrationIgnoresReusedSessionMessageWhenAuthenticationRecognized() { PremiumMessages messages = new PremiumMessagesDefaults().supply(new PremiumMessages()); - InspectablePremiumCompletionStep step = new InspectablePremiumCompletionStep(() -> messages); + InspectablePremiumCompletionStep step = new InspectablePremiumCompletionStep(() -> messages, recognizedStore(true)); List lines = step.lines(context(true, PipelineType.MIGRATION)); @@ -48,7 +50,7 @@ void migrationIgnoresReusedSessionMessageWhenAuthenticationRecognized() { @Test void migrationCompletionCopyAnnouncesPremiumMigrationCompletion() { PremiumMessages messages = new PremiumMessagesDefaults().supply(new PremiumMessages()); - InspectablePremiumCompletionStep step = new InspectablePremiumCompletionStep(() -> messages); + InspectablePremiumCompletionStep step = new InspectablePremiumCompletionStep(() -> messages, recognizedStore(true)); List lines = step.lines(context(true, PipelineType.MIGRATION)); @@ -60,7 +62,7 @@ void migrationCompletionCopyAnnouncesPremiumMigrationCompletion() { @Test void registrationUsesRegistrationMessageWhenConfigured() { PremiumMessages messages = new PremiumMessagesDefaults().supply(new PremiumMessages()); - InspectablePremiumCompletionStep step = new InspectablePremiumCompletionStep(() -> messages); + InspectablePremiumCompletionStep step = new InspectablePremiumCompletionStep(() -> messages, recognizedStore(false)); List lines = step.lines(context(false, PipelineType.REGISTRATION)); @@ -71,16 +73,17 @@ void registrationUsesRegistrationMessageWhenConfigured() { @Test void registrationIgnoresReusedSessionMessageWhenAuthenticationRecognized() { PremiumMessages messages = new PremiumMessagesDefaults().supply(new PremiumMessages()); - InspectablePremiumCompletionStep step = new InspectablePremiumCompletionStep(() -> messages); + InspectablePremiumCompletionStep step = new InspectablePremiumCompletionStep(() -> messages, recognizedStore(true)); List lines = step.lines(context(true, PipelineType.REGISTRATION)); assertEquals(messages.getCompletion().getRegistration().getBody(), lines); } - private CompletionContext context(boolean authenticationRecognized, PipelineType pipelineType) { + private CompletionContext context(boolean recognized, PipelineType pipelineType) { + TestIdentity identity = new TestIdentity(); return CompletionContext.builder() - .identity(new TestIdentity()) + .identity(identity) .pipelineType(pipelineType) .session(Session.builder() .uniqueId(UUID.randomUUID()) @@ -89,13 +92,21 @@ private CompletionContext context(boolean authenticationRecognized, PipelineType .originalUsername("PlayerOne") .effectiveUsername("PlayerOne") .build()) - .authenticationRecognized(authenticationRecognized) .build(); } + private RecognizedConnectionStore recognizedStore(boolean recognized) { + RecognizedConnectionStore store = mock(RecognizedConnectionStore.class); + when(store.isRecognized(any())).thenReturn(recognized); + return store; + } + private static final class InspectablePremiumCompletionStep extends PremiumCompletionStep { - private InspectablePremiumCompletionStep(Provider messagesProvider) { - super(messagesProvider); + private InspectablePremiumCompletionStep( + Provider messagesProvider, + RecognizedConnectionStore recognizedConnectionStore + ) { + super(messagesProvider, recognizedConnectionStore); } private List lines(CompletionContext context) { @@ -105,7 +116,7 @@ private List lines(CompletionContext context) { private static final class TestIdentity extends Identity { private TestIdentity() { - super(UUID.randomUUID(), "PlayerOne"); + super(UUID.randomUUID(), UUID.randomUUID(), "PlayerOne", "127.0.0.1"); } @Override diff --git a/identica-provider/provider-premium/premium/src/test/java/me/whereareiam/identica/provider/premium/step/FinalizeProfileStepTest.java b/identica-provider/provider-premium/premium/src/test/java/me/whereareiam/identica/provider/premium/step/FinalizeProfileStepTest.java index 88a01019c..22c868eb1 100644 --- a/identica-provider/provider-premium/premium/src/test/java/me/whereareiam/identica/provider/premium/step/FinalizeProfileStepTest.java +++ b/identica-provider/provider-premium/premium/src/test/java/me/whereareiam/identica/provider/premium/step/FinalizeProfileStepTest.java @@ -10,6 +10,7 @@ import me.whereareiam.identica.model.registration.RegistrationContext; import me.whereareiam.identica.pipeline.state.PipelineStateStore; import me.whereareiam.identica.provider.premium.config.PremiumMessages; +import me.whereareiam.identica.provider.premium.pipeline.step.shared.FinalizeProfileStep; import me.whereareiam.identica.provider.premium.profile.PremiumProfileSnapshot; import me.whereareiam.identica.provider.premium.profile.PremiumProfileStore; import org.junit.jupiter.api.BeforeEach; @@ -54,7 +55,7 @@ void finalizeProfileCompletesRegistration() { .build(); context.setProvider(ProviderContext.of("premium", null, "whereareiam", null)); when(profileStore.find("whereareiam")).thenReturn(new PremiumProfileSnapshot("premium-subject", System.currentTimeMillis())); - when(pipelineStateStore.find(org.mockito.ArgumentMatchers.any(me.whereareiam.identica.model.pipeline.state.PipelineStateReference.class))) + when(pipelineStateStore.find(org.mockito.ArgumentMatchers.any(me.whereareiam.identica.pipeline.state.PipelineStateReference.class))) .thenReturn(java.util.Optional.empty()); StepResult result = step.execute(context).join(); @@ -72,7 +73,7 @@ void finalizeProfileContinuesAuthentication() { .provider(ProviderContext.of("premium", null, "whereareiam", null)) .build(); when(profileStore.find("whereareiam")).thenReturn(new PremiumProfileSnapshot("premium-subject", System.currentTimeMillis())); - when(pipelineStateStore.find(org.mockito.ArgumentMatchers.any(me.whereareiam.identica.model.pipeline.state.PipelineStateReference.class))) + when(pipelineStateStore.find(org.mockito.ArgumentMatchers.any(me.whereareiam.identica.pipeline.state.PipelineStateReference.class))) .thenReturn(java.util.Optional.empty()); StepResult result = step.execute(context).join(); @@ -91,7 +92,7 @@ void finalizeProfileContinuesMigration() { .build(); context.setProvider(ProviderContext.of("premium", null, "whereareiam", null)); when(profileStore.find("whereareiam")).thenReturn(new PremiumProfileSnapshot("premium-subject", System.currentTimeMillis())); - when(pipelineStateStore.find(org.mockito.ArgumentMatchers.any(me.whereareiam.identica.model.pipeline.state.PipelineStateReference.class))) + when(pipelineStateStore.find(org.mockito.ArgumentMatchers.any(me.whereareiam.identica.pipeline.state.PipelineStateReference.class))) .thenReturn(java.util.Optional.empty()); StepResult result = step.execute(context).join(); diff --git a/identica-provider/provider-premium/premium/src/test/java/me/whereareiam/identica/provider/premium/step/OfflineCheckStepTest.java b/identica-provider/provider-premium/premium/src/test/java/me/whereareiam/identica/provider/premium/step/OfflineCheckStepTest.java index 48cdc81ca..d9c1eaadb 100644 --- a/identica-provider/provider-premium/premium/src/test/java/me/whereareiam/identica/provider/premium/step/OfflineCheckStepTest.java +++ b/identica-provider/provider-premium/premium/src/test/java/me/whereareiam/identica/provider/premium/step/OfflineCheckStepTest.java @@ -8,6 +8,7 @@ import me.whereareiam.identica.pipeline.state.PipelineStateStore; import me.whereareiam.identica.provider.ProviderAttemptStore; import me.whereareiam.identica.provider.premium.config.PremiumMessages; +import me.whereareiam.identica.provider.premium.pipeline.step.shared.OfflineCheckStep; import me.whereareiam.identica.provider.premium.profile.PremiumProfileSnapshot; import me.whereareiam.identica.provider.premium.profile.PremiumProfileStore; import me.whereareiam.identica.util.UniqueIdGenerator; diff --git a/identica-provider/provider-premium/premium/src/test/java/me/whereareiam/identica/provider/premium/step/PremiumRecognitionStepTest.java b/identica-provider/provider-premium/premium/src/test/java/me/whereareiam/identica/provider/premium/step/PremiumRecognitionStepTest.java index a0567331e..ed8fefe94 100644 --- a/identica-provider/provider-premium/premium/src/test/java/me/whereareiam/identica/provider/premium/step/PremiumRecognitionStepTest.java +++ b/identica-provider/provider-premium/premium/src/test/java/me/whereareiam/identica/provider/premium/step/PremiumRecognitionStepTest.java @@ -1,16 +1,16 @@ package me.whereareiam.identica.provider.premium.step; import me.whereareiam.identica.identity.actor.ConnectionIdentity; -import me.whereareiam.identica.identity.session.recognition.SessionRecognitionService; import me.whereareiam.identica.model.auth.AuthContext; import me.whereareiam.identica.model.pipeline.journey.stage.step.StepResult; import me.whereareiam.identica.model.provider.ProviderContext; -import me.whereareiam.identica.pipeline.journey.step.type.AuthenticationRecognitionStep; +import me.whereareiam.identica.provider.capability.recognition.SessionRecognitionService; +import me.whereareiam.identica.provider.capability.recognition.store.RecognizedConnectionStore; +import me.whereareiam.identica.provider.premium.pipeline.step.type.authentication.PremiumRecognitionStep; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @@ -20,34 +20,38 @@ class PremiumRecognitionStepTest { @Test void completesWhenRecognitionMatches() { SessionRecognitionService recognitionService = mock(SessionRecognitionService.class); + RecognizedConnectionStore recognizedConnectionStore = mock(RecognizedConnectionStore.class); when(recognitionService.matches(any(), any(), any(), any(), any())).thenReturn(true); - PremiumRecognitionStep step = new PremiumRecognitionStep(recognitionService); + PremiumRecognitionStep step = new PremiumRecognitionStep(recognitionService, recognizedConnectionStore); AuthContext context = context(); StepResult result = step.execute(context).join(); assertEquals(StepResult.StepStatus.COMPLETE, result.getStatus()); - assertTrue(step instanceof AuthenticationRecognitionStep); verify(recognitionService).matches("premium", "premium-subject", "whereareiam", "127.0.0.1", null); + verify(recognizedConnectionStore).markRecognized(context.getIdentity().getConnectionUniqueId()); } @DisplayName("Continues to verification when shared session recognition does not match") @Test void continuesWhenRecognitionDoesNotMatch() { SessionRecognitionService recognitionService = mock(SessionRecognitionService.class); + RecognizedConnectionStore recognizedConnectionStore = mock(RecognizedConnectionStore.class); when(recognitionService.matches(any(), any(), any(), any(), any())).thenReturn(false); - PremiumRecognitionStep step = new PremiumRecognitionStep(recognitionService); + PremiumRecognitionStep step = new PremiumRecognitionStep(recognitionService, recognizedConnectionStore); AuthContext context = context(); StepResult result = step.execute(context).join(); assertEquals(StepResult.StepStatus.CONTINUE, result.getStatus()); - assertTrue(step instanceof AuthenticationRecognitionStep); + verify(recognizedConnectionStore, never()).markRecognized(any()); } private AuthContext context() { + ConnectionIdentity identity = new ConnectionIdentity("whereareiam", "127.0.0.1"); + identity.setConnectionUniqueId(java.util.UUID.randomUUID()); return AuthContext.builder() - .identity(new ConnectionIdentity("whereareiam", "127.0.0.1")) + .identity(identity) .provider(ProviderContext.of("premium", "premium-subject", "whereareiam", null)) .build(); } diff --git a/scriptorium.project.json b/scriptorium.project.json index dd885176f..69412d85e 100644 --- a/scriptorium.project.json +++ b/scriptorium.project.json @@ -3,9 +3,38 @@ "description": "Provider-oriented Minecraft identity plugin.", "logo": "docs/assets/logo.png", "favicon": "docs/assets/favicon.ico", + "state": { + "bundling": { + "captions": [ + "Linking identity to the right provider.", + "Asking the providers who you really are.", + "Aligning providers, pipelines, and poor life choices.", + "Checking whether this username has paperwork.", + "Waking the routing goblins.", + "Reversing the authentication polarity.", + "Negotiating with the verification gremlins." + ] + } + }, "urls": { "github": "https://github.com/whereareiam/Identica" }, + "links": [ + { + "type": "icon", + "label": "GitHub", + "icon": "github", + "url": "https://github.com/whereareiam/Identica", + "external": true + }, + { + "type": "icon", + "label": "Discord", + "icon": "discord", + "url": "https://discord.arcadeya.com", + "external": true + } + ], "versions": { "home": "dev", "include": [ diff --git a/settings.gradle.kts b/settings.gradle.kts index 05408b3b2..d16499e9f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -5,6 +5,7 @@ pluginManagement { includeBuild("build-logic") repositories { + mavenLocal() gradlePluginPortal() mavenCentral() maven("https://maven.whereareiam.me/release") @@ -76,6 +77,69 @@ project(":platform-bungeecord-bootstrap").projectDir = file("identica-platform/p include(":identica-provider") project(":identica-provider").projectDir = file("identica-provider") +include(":identica-feature") +project(":identica-feature").projectDir = file("identica-feature") + +include(":identica-capability") +project(":identica-capability").projectDir = file("identica-capability") + +include(":feature-verification") +project(":feature-verification").projectDir = file("identica-feature/feature-verification/verification") + +include(":feature-verification-api") +project(":feature-verification-api").projectDir = file("identica-feature/feature-verification/verification-api") + +include(":feature-verification-common") +project(":feature-verification-common").projectDir = file("identica-feature/feature-verification/verification-common") + +include(":feature-verification-database") +project(":feature-verification-database").projectDir = file("identica-feature/feature-verification/verification-database") + +include(":capability-authoritative-username") +project(":capability-authoritative-username").projectDir = + file("identica-capability/capability-authoritative-username/authoritative-username") + +include(":capability-authoritative-username-api") +project(":capability-authoritative-username-api").projectDir = + file("identica-capability/capability-authoritative-username/authoritative-username-api") + +include(":capability-authoritative-username-common") +project(":capability-authoritative-username-common").projectDir = + file("identica-capability/capability-authoritative-username/authoritative-username-common") + +include(":capability-authoritative-username-database") +project(":capability-authoritative-username-database").projectDir = + file("identica-capability/capability-authoritative-username/authoritative-username-database") + +include(":capability-recognition") +project(":capability-recognition").projectDir = file("identica-capability/capability-recognition/recognition") + +include(":capability-recognition-api") +project(":capability-recognition-api").projectDir = file("identica-capability/capability-recognition/recognition-api") + +include(":capability-recognition-common") +project(":capability-recognition-common").projectDir = file("identica-capability/capability-recognition/recognition-common") + +include(":capability-restriction") +project(":capability-restriction").projectDir = file("identica-capability/capability-restriction/restriction") + +include(":capability-restriction-api") +project(":capability-restriction-api").projectDir = file("identica-capability/capability-restriction/restriction-api") + +include(":capability-restriction-common") +project(":capability-restriction-common").projectDir = file("identica-capability/capability-restriction/restriction-common") + +include(":capability-restriction-join") +project(":capability-restriction-join").projectDir = file("identica-capability/capability-restriction/restriction-join/join") + +include(":capability-restriction-join-api") +project(":capability-restriction-join-api").projectDir = + file("identica-capability/capability-restriction/restriction-join/join-api") + +include(":capability-restriction-join-common") +project(":capability-restriction-join-common").projectDir = + file("identica-capability/capability-restriction/restriction-join/join-common") + include(":provider-credential") project(":provider-credential").projectDir = file("identica-provider/provider-credential")