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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ coordinate is the reactor parent (a `pom`, no classes) — depend on one of the

**À la carte:** import the BOM once, then declare `spring-services-core` plus only the feature
modules you need (`spring-services-slack`, `spring-services-mcp`, `spring-services-email`,
`spring-services-search`, `spring-services-dbbackup`, `spring-services-scim`) without versions:
`spring-services-search`, `spring-services-dbbackup`, `spring-services-scim`,
`spring-services-tenant`) without versions:

```xml
<dependencyManagement>
Expand Down Expand Up @@ -331,6 +332,7 @@ spring-services/ — reactor parent (packaging=pom)
├── spring-services-search — Meilisearch full-text search (RestClient, no extra dep)
├── spring-services-dbbackup — db-backup sidecar client (RestClient, no extra dep)
├── spring-services-scim — SCIM 2.0 Users provider (opt-in via openelements.scim.token)
├── spring-services-tenant — row-level multi-tenancy (self-activates on the classpath)
├── spring-services-all — everything bundle (depends on all modules; no config of its own)
└── spring-services-bom — bill of materials for lockstep versioning
```
Expand Down
31 changes: 30 additions & 1 deletion docs/releases/upgrade-to-1.4.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
# Upgrade prompt: enabling the SCIM 2.0 Users provider (spec 015)
# Upgrade prompt: spring-services 1.4.0 optional-module changes

`spring-services` 1.4.0 adds the optional **SCIM 2.0 Users provider** (spec 015) and moves
**multi-tenancy into its own module** (spec 016). Both sections below are independent — apply only
the ones you use.

## SCIM 2.0 Users provider (spec 015)

`spring-services` 1.4.0 adds an **optional** SCIM 2.0 Users service-provider module,
`spring-services-scim`. It lets an external identity provider (Authentik, or any RFC 7644 client)
Expand Down Expand Up @@ -83,3 +89,26 @@ credential. Rotation = change this property and the IdP token together; there is
group write returns `501 Not Implemented`. Do not enable group provisioning in your IdP yet.
- User `PATCH` is not implemented (`501`); Authentik updates via `PUT` full-replace.
- Do not point your **application's own** entities at the reserved SCIM principal UUID.

## Multi-tenancy moved to `spring-services-tenant` (spec 016)

The multi-tenancy abstractions (`TenantService`, `AbstractMultitenantEntity`,
`AbstractMultitenantDbBackedDataService`, `RepositoryWithTenantSupport`, `EnableTenant`,
`TenantConfig`, …) previously lived in `spring-services-core`. In 1.4.0 they move, unchanged, into a
new optional feature module `spring-services-tenant`. No API, schema, or runtime-behaviour change to
the tenancy logic — this is a build-coordinate change only.

- **`spring-services-all` consumers: nothing to do.** The everything-bundle now depends on
`spring-services-tenant`, so the feature is present exactly as before.
- **À-la-carte consumers who used tenant types via `spring-services-core`:** add the module
(version managed by the BOM):

```xml
<dependency>
<groupId>com.open-elements</groupId>
<artifactId>spring-services-tenant</artifactId>
</dependency>
```

The feature self-activates on classpath presence (`TenantAutoConfiguration`); `@EnableTenant` and
`@Import(TenantConfig.class)` continue to work for explicit opt-in. There is no database migration.
2 changes: 1 addition & 1 deletion docs/specs/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@
| 013 | 013-dedicated-schema-and-starter | Dedicated schema + starter | backend, database, architecture, build | Move all 7 library tables into a fixed `oe_spring_services` schema (single persistence unit) and ship a real Spring Boot starter that registers the library package additively via AutoConfigurationPackages — zero-config consumption. Migrations stay consumer-managed; the library ships ready-to-apply per-release SQL in the upgrade doc. Includes the app→lib cross-entity reference contract | #25 | done |
| 014 | 014-multi-module-restructuring | Multi-module restructuring | build, architecture, infrastructure | Restructure the single artifact into a Maven reactor: `spring-services-core` (all 7 entities, single persistence unit) + optional feature modules (`slack`, `mcp`, `email`, `search`, `dbbackup`) + `spring-services-all` aggregator jar + `spring-services-bom`. Lockstep versioning; per-module `@AutoConfiguration` guarded by `@ConditionalOnClass`; builds on spec 013 (released first). Breaking change: `spring-services` coordinate becomes the reactor parent, consumers migrate to `spring-services-all` | #27 | done |
| 015 | 015-scim-users-provider | SCIM Users provider | backend, security, api, database | SCIM 2.0 service-provider first slice: an isolated `/scim/v2/**` bearer-token filter chain (static `openelements.scim.token`), discovery endpoints (ServiceProviderConfig/ResourceTypes/Schemas), and the Users resource (list/filter, POST, GET, PUT full-replace, soft DELETE) mapped onto spec 012's `UserEntity`. `POST` collision → `409 uniqueness`; DELETE is soft (`deleted`/`deleted_at`); writes audited under a reserved SCIM service principal. Groups+membership and Group→Role mapping deferred to follow-up issues; grounded in real Authentik 2026.5.4 traffic (#21) with a pre-merge verification test plan. | #21 | done |
| 016 | 016-tenant-feature-module | Tenant feature module | build, architecture | Extract the `com.openelements.spring.base.tenant` package out of `spring-services-core` into a new optional feature module `spring-services-tenant` (depends on core), consistent with the spec-014 module pattern. Self-activates via `TenantAutoConfiguration`; `TenantConfig` drops its `@ComponentScan` in favour of an explicit `TenantService` `@Bean` to avoid double registration. Core loses its only two tenant references (`FullSpringServiceConfig` `@Import`, a `data/package-info.java` Javadoc link). Added to reactor + `spring-services-all` + BOM. Build-coordinate change only (no API/schema/behaviour change); breaking for à-la-carte core consumers of tenant types → documented in the 1.4.0 upgrade guide. | — | open |
| 016 | 016-tenant-feature-module | Tenant feature module | build, architecture | Extract the `com.openelements.spring.base.tenant` package out of `spring-services-core` into a new optional feature module `spring-services-tenant` (depends on core), consistent with the spec-014 module pattern. Self-activates via `TenantAutoConfiguration`; `TenantConfig` drops its `@ComponentScan` in favour of an explicit `TenantService` `@Bean` to avoid double registration. Core loses its only two tenant references (`FullSpringServiceConfig` `@Import`, a `data/package-info.java` Javadoc link). Added to reactor + `spring-services-all` + BOM. Build-coordinate change only (no API/schema/behaviour change); breaking for à-la-carte core consumers of tenant types → documented in the 1.4.0 upgrade guide. | #34 | done |

1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
<module>spring-services-email</module>
<module>spring-services-mcp</module>
<module>spring-services-scim</module>
<module>spring-services-tenant</module>
<module>spring-services-all</module>
<module>spring-services-bom</module>
</modules>
Expand Down
5 changes: 5 additions & 0 deletions spring-services-all/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@
<artifactId>spring-services-scim</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.open-elements</groupId>
<artifactId>spring-services-tenant</artifactId>
<version>${project.version}</version>
</dependency>

<!-- Test dependencies: the aggregate test boots the full classpath against Postgres. -->
<dependency>
Expand Down
5 changes: 5 additions & 0 deletions spring-services-bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@
<artifactId>spring-services-scim</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.open-elements</groupId>
<artifactId>spring-services-tenant</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.open-elements</groupId>
<artifactId>spring-services-all</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import com.openelements.spring.base.services.tag.TagConfig;
import com.openelements.spring.base.services.translation.TranslationConfig;
import com.openelements.spring.base.services.webhook.WebhookConfig;
import com.openelements.spring.base.tenant.TenantConfig;
import org.springframework.context.annotation.Import;

/**
Expand All @@ -29,8 +28,6 @@
*
* <ul>
* <li>{@link SecurityConfig} — JWT and API-key authentication.
* <li>{@link TenantConfig} — multi-tenancy support (entities, repositories, current-tenant
* resolution).
* <li>{@link ApiKeyConfig} — API-key data layer (entity, repository, data service).
* <li>{@link SettingsConfig} — generic key/value settings store.
* <li>{@link TagConfig} — taggable-entity support.
Expand All @@ -40,7 +37,6 @@
*/
@Import({
SecurityConfig.class,
TenantConfig.class,
ApiKeyConfig.class,
SettingsConfig.class,
TagConfig.class,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@
*
* <h2>Tenant-aware variants</h2>
*
* <p>For multi-tenant services, see {@link com.openelements.spring.base.tenant} which provides the
* tenant-scoped counterparts of the abstractions defined here.
* <p>For multi-tenant services, see the {@code spring-services-tenant} module (package
* {@code com.openelements.spring.base.tenant}), which provides the tenant-scoped counterparts of the
* abstractions defined here.
*/
package com.openelements.spring.base.data;

This file was deleted.

32 changes: 32 additions & 0 deletions spring-services-tenant/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>com.open-elements</groupId>
<artifactId>spring-services</artifactId>
<version>1.4.0-SNAPSHOT</version>
</parent>

<artifactId>spring-services-tenant</artifactId>

<name>Spring Services Tenant</name>
<description>Optional row-level multi-tenancy feature module for spring-services</description>
<url>https://github.com/OpenElementsLabs/spring-services</url>

<dependencies>
<dependency>
<groupId>com.open-elements</groupId>
<artifactId>spring-services-core</artifactId>
<version>${project.version}</version>
</dependency>

<!-- Test dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.openelements.spring.base.tenant;

import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Import;

/**
* Auto-configuration for the optional multi-tenancy feature.
*
* <p>Registered through this module's
* {@code META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports}, so the
* feature self-activates whenever {@code spring-services-tenant} is on the classpath. The module
* ships no optional third-party library, so — unlike the slack/mcp/email auto-configurations — there
* is no {@code @ConditionalOnClass} guard: present on the classpath means active.
*
* <p>Co-locating in the {@code tenant} package is safe because {@link TenantConfig} registers
* {@link TenantService} through an explicit {@code @Bean} instead of a {@code @ComponentScan} that
* would otherwise sweep up this class. {@code @Import(TenantConfig.class)} / {@link EnableTenant}
* remain valid for explicit wiring.
*/
@AutoConfiguration
@Import(TenantConfig.class)
public class TenantAutoConfiguration {

/** Creates the tenant auto-configuration; wiring is delegated to {@link TenantConfig}. */
public TenantAutoConfiguration() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.openelements.spring.base.tenant;

import com.openelements.spring.base.security.AuthService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* Spring configuration for the multi-tenancy feature.
*
* <p>Registers {@link TenantService} as an explicit {@code @Bean} (rather than component-scanning
* the package) so that {@link TenantAutoConfiguration}, which lives in the same package, is not
* swept up and registered twice. Can be imported directly, via {@link EnableTenant}, or by the
* module's auto-configuration.
*/
@Configuration(proxyBeanMethods = false)
public class TenantConfig {

/** Creates the configuration. */
public TenantConfig() {}

/**
* Registers the tenant service.
*
* @param authService the service used to resolve the authenticated principal
* @return the tenant service
*/
@Bean
public TenantService tenantService(final AuthService authService) {
return new TenantService(authService);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@

import com.openelements.spring.base.security.AuthService;
import java.util.Objects;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
* Resolves the tenant id of the current request.
*
* <p>The tenant id is taken verbatim from the {@code name} of the authenticated principal as
* exposed by {@link AuthService#getPrincipal()}. For JWT-authenticated requests this is the {@code
* sub} claim; for API-key requests it is the API key entity's name.
*
* <p>Registered as a bean by {@link TenantConfig#tenantService(AuthService)} rather than by
* component scanning.
*/
@Service
public class TenantService {

private final AuthService authService;
Expand All @@ -22,7 +22,6 @@ public class TenantService {
*
* @param authService the service used to resolve the authenticated principal
*/
@Autowired
public TenantService(final AuthService authService) {
this.authService = Objects.requireNonNull(authService, "authService must not be null");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
com.openelements.spring.base.tenant.TenantAutoConfiguration
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.openelements.spring.base.tenant;

import static org.assertj.core.api.Assertions.assertThat;

import com.openelements.spring.base.security.AuthService;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;

/**
* Verifies that {@link TenantAutoConfiguration} self-activates and registers exactly one
* {@link TenantService} bean — i.e. the switch from {@code @ComponentScan} to an explicit
* {@code @Bean} in {@link TenantConfig} does not double-register the service.
*/
@DisplayName("TenantAutoConfiguration — self-activation")
class TenantAutoConfigurationTest {

private final ApplicationContextRunner contextRunner =
new ApplicationContextRunner()
.withBean(AuthService.class, AuthService::new)
.withConfiguration(AutoConfigurations.of(TenantAutoConfiguration.class));

@Test
@DisplayName("Exactly one TenantService bean is registered on the classpath")
void singleTenantServiceBean() {
contextRunner.run(
context -> assertThat(context).hasNotFailed().hasSingleBean(TenantService.class));
}
}