Compare commits

...

7 commits

Author SHA1 Message Date
324fe35f5a
add note on how to get an example keycloak setup for the example setup 2026-03-31 16:03:59 +02:00
bd9e5a6f3e
add Makefile and docs on how to build the provider 2026-03-31 16:03:59 +02:00
964a593dbd
remove emojis from example in README as well 2026-03-31 15:58:02 +02:00
85bd0c6572
only perform further validation, if config string isn't null
For the configAttributeSlug the further validation fails ugly otherwise
and there's generally no need to do further validation, if a config
string is null.
2026-03-31 15:54:39 +02:00
5e791af057
add null checks 2026-03-31 14:31:07 +02:00
393284ebdc
remove realm exports 2026-03-31 14:07:25 +02:00
ffa6c92c65
renaming and sort some code 2026-03-31 14:05:24 +02:00
6 changed files with 45 additions and 5213 deletions

7
Makefile Normal file
View file

@ -0,0 +1,7 @@
verify:
mvn -f attribute-endpoints-provider verify
clean:
mvn -f attribute-endpoints-provider clean
.PHONY: verify clean

View file

@ -5,17 +5,25 @@ For this it will provide API endpoints for every configured attribute-group.
The configuration of the provider is possible via an admin page. The configuration of the provider is possible via an admin page.
Every endpoint responds with a list of all attribute values, that: Every endpoint responds with a list of all attribute values, that:
- is in the attribute group matching `attribute-group` - are in the attribute group matching `attribute-group`
- matches an optional RegEx Pattern `attribute-regex` - match an optional RegEx Pattern `attribute-regex`
- belongs to a user with a role matching `match-role` - belong to a user with a role matching `match-role`
- is non-empty - are non-empty
Multivalue attributes are flattened in the response. Multivalue attributes are flattened in the response.
## Building
Maven is required for building the provider.
Once all dependencies are met, simply call `make` to build the provider, which should then produce a `attribute-endpoints-provider-1.0-SNAPSHOT.jar` in the `attribute-endpoints-provider/target/` directory.
There's also `make clean` available for removing the output directory.
## Example Setup ## Example Setup
We assume an unconfigured, fresh Keycloak installation running under `http://localhost:8080`. We assume an unconfigured, fresh Keycloak installation running under `http://localhost:8080`.
(This can be achieved by running the provided `compose.yaml` after building the provider as outlined in [Building](#building).)
1. Add a new realm 1. Add a new realm
e.g. "TestRealm" e.g. "TestRealm"
@ -53,7 +61,7 @@ We assume an unconfigured, fresh Keycloak installation running under `http://loc
- After creating: - After creating:
- give it the role `myattribute-export` - give it the role `myattribute-export`
- set a password in the users settings `Creadentials > Set password`. For Example `"password"` - set a password in the users settings `Creadentials > Set password`. For Example `"password"`
8. Under `🪪 Attribute Endpoints 🚀 > Create item`, add a new endpoint to the provider 8. Under `Attribute Endpoints > Create item`, add a new endpoint to the provider
Example: Example:
- `Slug` = `"ssh_keys"` - `Slug` = `"ssh_keys"`
- `Attribute Group` = `"my-attributes-group"` - `Attribute Group` = `"my-attributes-group"`

View file

@ -24,7 +24,7 @@ import com.google.auto.service.AutoService;
*/ */
@AutoService(UiPageProviderFactory.class) @AutoService(UiPageProviderFactory.class)
public class AdminUiPage implements UiPageProvider, UiPageProviderFactory<ComponentModel> { public class AdminUiPage implements UiPageProvider, UiPageProviderFactory<ComponentModel> {
public static final String PROVIDER_ID = "🪪 Attribute Endpoints 🚀"; public static final String PROVIDER_ID = "Attribute Endpoints";
@Override @Override
public void init(Config.Scope config) { public void init(Config.Scope config) {
@ -54,22 +54,28 @@ public class AdminUiPage implements UiPageProvider, UiPageProviderFactory<Compon
Pattern slugPattern = Pattern.compile("^[a-zA-Z0-9_-]*$"); Pattern slugPattern = Pattern.compile("^[a-zA-Z0-9_-]*$");
String configAttributeSlug = model.getConfig().getFirst("slug"); String configAttributeSlug = model.getConfig().getFirst("slug");
if (configAttributeSlug == null) {
if (!slugPattern.matcher(configAttributeSlug).matches()) { hasError = true;
errorString += " • [Slug] can not be empty\n";
} else if (!slugPattern.matcher(configAttributeSlug).matches()) {
hasError = true; hasError = true;
errorString += " • [Slug] can only contain anlphanumeric characters, dash and underscore (a-z A-Z 0-9 _ - )\n"; errorString += " • [Slug] can only contain anlphanumeric characters, dash and underscore (a-z A-Z 0-9 _ - )\n";
} }
String configAuthRole = model.getConfig().getFirst("auth-role"); String configAuthRole = model.getConfig().getFirst("auth-role");
RoleModel authRole = realm.getRole(configAuthRole); if (configAuthRole == null) {
if (authRole == null) { hasError = true;
errorString += " • [Auth Role] can not be empty\n";
} else if (realm.getRole(configAuthRole) == null) {
hasError = true; hasError = true;
errorString += " • [Auth Role] does not exist\n"; errorString += " • [Auth Role] does not exist\n";
} }
String configMatchRole = model.getConfig().getFirst("match-role"); String configMatchRole = model.getConfig().getFirst("match-role");
RoleModel matchRole = realm.getRole(configMatchRole); if (configMatchRole == null) {
if (matchRole == null) { hasError = true;
errorString += " • [Match Role] can not be empty\n";
} else if (realm.getRole(configMatchRole) == null) {
hasError = true; hasError = true;
errorString += " • [Match Role] does not exist\n"; errorString += " • [Match Role] does not exist\n";
} }
@ -77,7 +83,10 @@ public class AdminUiPage implements UiPageProvider, UiPageProviderFactory<Compon
UserProfileProvider profileProvider = session.getProvider(UserProfileProvider.class); UserProfileProvider profileProvider = session.getProvider(UserProfileProvider.class);
UPConfig upconfig = profileProvider.getConfiguration(); UPConfig upconfig = profileProvider.getConfiguration();
String configAttributeGroup = model.getConfig().getFirst("attribute-group"); String configAttributeGroup = model.getConfig().getFirst("attribute-group");
if (!upconfig.getGroups().stream().anyMatch(g -> g.getName().equals(configAttributeGroup))) { if (configAttributeGroup == null) {
hasError = true;
errorString += " • [Attribute Group] can not be empty\n";
} else if (!upconfig.getGroups().stream().anyMatch(g -> g.getName().equals(configAttributeGroup))) {
hasError = true; hasError = true;
errorString += " • [Attribute Group] does not exist\n"; errorString += " • [Attribute Group] does not exist\n";
} }

View file

@ -53,7 +53,7 @@ public class AttributeEndpointsResourceProvider implements RealmResourceProvider
@GET @GET
@Path("export/{slug}") @Path("export/{slug}")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public Response exportKeys(@PathParam("slug") String slug) { public Response exportAttributeValues(@PathParam("slug") String slug) {
KeycloakContext context = session.getContext(); KeycloakContext context = session.getContext();
RealmModel realm = context.getRealm(); RealmModel realm = context.getRealm();
@ -62,11 +62,16 @@ public class AttributeEndpointsResourceProvider implements RealmResourceProvider
.filter(c -> c.getConfig().getFirst("slug").equals(slug)) .filter(c -> c.getConfig().getFirst("slug").equals(slug))
.toList(); .toList();
Auth auth = AttributeEndpointsResourceProvider.getAuth(session);
if (componentList.isEmpty()) { if (componentList.isEmpty()) {
throw new NotFoundException("Endpoint not found."); throw new NotFoundException("Endpoint not found.");
} }
Auth auth = AttributeEndpointsResourceProvider.getAuth(session); if (componentList.size() > 1) {
throw new NotFoundException(
"Endpoint Configuration Error - Multiple configurations exist for this endpoint.");
}
ComponentModel component = componentList.get(0); ComponentModel component = componentList.get(0);
@ -106,11 +111,6 @@ public class AttributeEndpointsResourceProvider implements RealmResourceProvider
throw new ForbiddenException("User does not have required auth role."); throw new ForbiddenException("User does not have required auth role.");
} }
if (componentList.size() > 1) {
throw new NotFoundException(
"Endpoint Configuration Error - Multiple configurations exist for this endpoint.");
}
List<String> attributeNames = upconfig.getAttributes() List<String> attributeNames = upconfig.getAttributes()
.stream() .stream()
.filter(a -> a.getGroup() != null && a.getGroup().equals(configAttributeGroup)) .filter(a -> a.getGroup() != null && a.getGroup().equals(configAttributeGroup))

File diff suppressed because it is too large Load diff

2525
test.json

File diff suppressed because it is too large Load diff