Configure Extension with UiPageProvider #1
11 changed files with 5593 additions and 128 deletions
86
README.md
Normal file
86
README.md
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
# Attribute Endpoints Provider
|
||||
|
||||
This is a Keycloak Provider that exports an anonymized list of user profile attribute values.
|
||||
For this it will provide API endpoints for every configured attribute-group.
|
||||
The configuration of the provider is possible via an admin page.
|
||||
|
||||
Every endpoint responds with a list of all attribute values, that:
|
||||
- is in the attribute group matching `attribute-group`
|
||||
- matches an optional RegEx Pattern `attribute-regex`
|
||||
- belongs to a user with a role matching `match-role`
|
||||
- is non-empty
|
||||
|
|
||||
|
||||
Multivalue attributes are flattened in the response.
|
||||
|
||||
|
||||
## Example Setup
|
||||
|
||||
We assume an unconfigured, fresh Keycloak installation running under `http://localhost:8080`.
|
||||
|
||||
1. Add a new realm
|
||||
e.g. "TestRealm"
|
||||
2. Under `Realm Settings > User profile > Attributes Group`, add a new attribute Group
|
||||
Example:
|
||||
- `Name` = `"my-attributes-group"`
|
||||
- `Display name` = `"Endpoint Attributes"`
|
||||
- `Display description` = `"Attributes exported by the provider."`
|
||||
3. Under `Realm Settings > User profile > Attributes`, add a new attribute
|
||||
Example:
|
||||
- `Attribute [Name]` = `"ssh-keys"`
|
||||
- `Display name ` = `"SSH Keys"`
|
||||
- `Multivalued` = `On`
|
||||
- `Attribute group` = `"my-attributes-group"`
|
||||
- `Who can edit?` = `user, admin`
|
||||
- `Validators`
|
||||
You can add validators, which will limit what values the user can enter. These validators are ignored by the provider.
|
||||
4. Under `Realm roles`, add two new roles
|
||||
Example:
|
||||
1. `Role name` = `"myattribute-match"`
|
||||
2. `Role name` = `"myattribute-export"`
|
||||
5. Under `Users`, add a new user
|
||||
Example:
|
||||
- `Username` = `"user"`
|
||||
- `Email` = `"user@example.com"`
|
||||
- `First name` = `"User"`
|
||||
- `Last name` = `"User"`
|
||||
- `SSH Keys` = `"example-value-1", "example-value-2"`
|
||||
6. In the Settings of the newly created user, go to `Role mapping > Assing role > Realm roles` and check the role `myattribute-match`
|
||||
7. create a second user to use the provider
|
||||
- `Username` = `"bot-user"`
|
||||
- `Email` = `"bot@example.com"`
|
||||
- `First name` = `"Bot"`
|
||||
- `Last name` = `"Bot"`
|
||||
- After creating:
|
||||
- give it the role `myattribute-export`
|
||||
- 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
|
||||
Example:
|
||||
- `Slug` = `"ssh_keys"`
|
||||
- `Attribute Group` = `"my-attributes-group"`
|
||||
- `Match Role` = `"myattribute-match"`
|
||||
- `Auth Role` = `"myattribute-export"`
|
||||
- `Attribute RegEx` = `".*"`
|
||||
9. Aquire an OIDC Access Token:
|
||||
```shell
|
||||
curl --request POST \
|
||||
--url http://localhost:8080/realms/TestRealm/protocol/openid-connect/token \
|
||||
--header 'content-type: application/x-www-form-urlencoded' \
|
||||
--data scope=openid \
|
||||
--data username=bot-user \
|
||||
--data password=password \
|
||||
--data grant_type=password \
|
||||
--data client_id=admin-cli
|
||||
```
|
||||
10. copy the value of the response key `access_token` and use it in a second request:
|
||||
```shell
|
||||
curl --request GET \
|
||||
--url http://localhost:8080/realms/TestRealm/attribute-endpoints-provider/export/ssh_keys \
|
||||
--header 'authorization: Bearer ey...' \
|
||||
--header 'content-type: application/json'
|
||||
```
|
||||
11. You should get a response like this:
|
||||
```json
|
||||
["example-value-1","example-value-2"]
|
||||
```
|
||||
|
||||
Although this example uses a simple bot account to authenticate to Keycloak, we recommend using a client with service account, when using this provider programmatically.
|
||||
|
|
@ -5,11 +5,11 @@
|
|||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>de.ccc.hamburg.keycloak.ssh_key</groupId>
|
||||
<artifactId>ssh-key-provider</artifactId>
|
||||
<groupId>de.ccc.hamburg.keycloak.attribute_endpoints</groupId>
|
||||
<artifactId>attribute-endpoints-provider</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
|
||||
<name>ssh-key-provider</name>
|
||||
<name>attribute-endpoints-provider</name>
|
||||
<!-- FIXME change it to the project's website -->
|
||||
<url>http://www.example.com</url>
|
||||
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
package de.ccc.hamburg.keycloak.attribute_endpoints;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.component.ComponentValidationException;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.provider.ProviderConfigurationBuilder;
|
||||
import org.keycloak.representations.userprofile.config.UPConfig;
|
||||
import org.keycloak.services.ui.extend.UiPageProvider;
|
||||
import org.keycloak.services.ui.extend.UiPageProviderFactory;
|
||||
import org.keycloak.userprofile.UserProfileProvider;
|
||||
|
||||
|
june
commented
I think getting rid of the emojis, while they are fun, might be a good idea. I think getting rid of the emojis, while they are fun, might be a good idea.
|
||||
import com.google.auto.service.AutoService;
|
||||
|
||||
/**
|
||||
* Implements UiPageProvider to show a config page in the admin
|
||||
*/
|
||||
@AutoService(UiPageProviderFactory.class)
|
||||
public class AdminUiPage implements UiPageProvider, UiPageProviderFactory<ComponentModel> {
|
||||
public static final String PROVIDER_ID = "🪪 Attribute Endpoints 🚀";
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
public String getHelpText() {
|
||||
return "Configure endpoints of the Attribute Endpoint Provider.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) {
|
||||
String errorString = "\n";
|
||||
Boolean hasError = false;
|
||||
|
||||
Pattern slugPattern = Pattern.compile("^[a-zA-Z0-9_-]*$");
|
||||
String configAttributeSlug = model.getConfig().getFirst("slug");
|
||||
|
||||
if (!slugPattern.matcher(configAttributeSlug).matches()) {
|
||||
|
june
commented
Currently this gives us an ugly exception in the Keycloak logs for an empty slug, as Currently this gives us an ugly exception in the Keycloak logs for an empty slug, as `configAttributeSlug` is then `null`. Not critical as it still just errors, but we should probably check, if `configAttributeSlug` is `null`. I guess then adding a null check to all the other `config$thing`s is probably a good idea as well.
|
||||
hasError = true;
|
||||
errorString += " • [Slug] can only contain anlphanumeric characters, dash and underscore (a-z A-Z 0-9 _ - )\n";
|
||||
}
|
||||
|
||||
String configAuthRole = model.getConfig().getFirst("auth-role");
|
||||
RoleModel authRole = realm.getRole(configAuthRole);
|
||||
if (authRole == null) {
|
||||
hasError = true;
|
||||
errorString += " • [Auth Role] does not exist\n";
|
||||
}
|
||||
|
||||
String configMatchRole = model.getConfig().getFirst("match-role");
|
||||
RoleModel matchRole = realm.getRole(configMatchRole);
|
||||
if (matchRole == null) {
|
||||
hasError = true;
|
||||
errorString += " • [Match Role] does not exist\n";
|
||||
}
|
||||
|
||||
UserProfileProvider profileProvider = session.getProvider(UserProfileProvider.class);
|
||||
UPConfig upconfig = profileProvider.getConfiguration();
|
||||
String configAttributeGroup = model.getConfig().getFirst("attribute-group");
|
||||
if (!upconfig.getGroups().stream().anyMatch(g -> g.getName().equals(configAttributeGroup))) {
|
||||
hasError = true;
|
||||
errorString += " • [Attribute Group] does not exist\n";
|
||||
}
|
||||
|
||||
String configAttributeRegex = model.getConfig().getFirst("attribute-regex");
|
||||
Boolean regexIsBlank = configAttributeRegex == null;
|
||||
|
||||
if (!regexIsBlank) {
|
||||
try {
|
||||
Pattern.compile(configAttributeRegex);
|
||||
} catch (Exception e) {
|
||||
hasError = true;
|
||||
errorString += " • [Attribute RegEx] is not a valid regex pattern\n";
|
||||
}
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
throw new ComponentValidationException(errorString);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return ProviderConfigurationBuilder.create()
|
||||
.property()
|
||||
.name("slug")
|
||||
.label("Slug")
|
||||
.helpText(
|
||||
"The slug in the path of the API endpoint (e.g. /realms/:realm/attribute-endpoint-provider/export/:slug)")
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.add()
|
||||
|
||||
.property()
|
||||
.name("attribute-group")
|
||||
.label("Attribute Group")
|
||||
.helpText("The attribute group to export.")
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.add()
|
||||
|
||||
.property()
|
||||
.name("match-role")
|
||||
.label("Match Role")
|
||||
.helpText("Export only attributes of users with this role.")
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.add()
|
||||
|
||||
.property()
|
||||
.name("auth-role")
|
||||
.label("Auth Role")
|
||||
.helpText("Role needeed by the authenticated account to be able to use this endpoint.")
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.add()
|
||||
|
||||
.property()
|
||||
.name("attribute-regex")
|
||||
.label("Attribute RegEx")
|
||||
.helpText("A RegEx Rule used to verify each attribute value. Only matching values are returned.")
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.add()
|
||||
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
package de.ccc.hamburg.keycloak.attribute_endpoints;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakContext;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserProvider;
|
||||
import org.keycloak.representations.userprofile.config.UPConfig;
|
||||
import org.keycloak.services.managers.AppAuthManager;
|
||||
import org.keycloak.services.managers.Auth;
|
||||
import org.keycloak.services.managers.AuthenticationManager.AuthResult;
|
||||
import org.keycloak.services.resource.RealmResourceProvider;
|
||||
import org.keycloak.userprofile.UserProfileProvider;
|
||||
|
||||
import jakarta.ws.rs.ForbiddenException;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.NotAuthorizedException;
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.ServerErrorException;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
|
||||
public class AttributeEndpointsResourceProvider implements RealmResourceProvider {
|
||||
private static final Logger LOG = Logger.getLogger(AttributeEndpointsResourceProvider.class);
|
||||
private final KeycloakSession session;
|
||||
|
||||
public AttributeEndpointsResourceProvider(KeycloakSession keycloakSession) {
|
||||
this.session = keycloakSession;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getResource() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("export/{slug}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response exportKeys(@PathParam("slug") String slug) {
|
||||
KeycloakContext context = session.getContext();
|
||||
|
june
commented
I guess this should be called something like I guess this should be called something like `exportAttributeValues` now that this is not just about SSH Keys anymore.
|
||||
RealmModel realm = context.getRealm();
|
||||
|
||||
List<ComponentModel> componentList = realm.getComponentsStream()
|
||||
.filter(c -> c.getProviderId().equals(AdminUiPage.PROVIDER_ID))
|
||||
.filter(c -> c.getConfig().getFirst("slug").equals(slug))
|
||||
.toList();
|
||||
|
||||
if (componentList.isEmpty()) {
|
||||
throw new NotFoundException("Endpoint not found.");
|
||||
}
|
||||
|
||||
Auth auth = AttributeEndpointsResourceProvider.getAuth(session);
|
||||
|
||||
ComponentModel component = componentList.get(0);
|
||||
|
||||
String configAuthRole = component.getConfig().getFirst("auth-role");
|
||||
RoleModel authRole = realm.getRole(configAuthRole);
|
||||
if (authRole == null) {
|
||||
throw new ServerErrorException("Endpoint Configuration Error - auth-role does not exist.", 500);
|
||||
}
|
||||
|
||||
String configMatchRole = component.getConfig().getFirst("match-role");
|
||||
RoleModel matchRole = realm.getRole(configMatchRole);
|
||||
if (matchRole == null) {
|
||||
throw new ServerErrorException("Endpoint Configuration Error - match-role does not exist.", 500);
|
||||
}
|
||||
|
||||
UserProfileProvider profileProvider = session.getProvider(UserProfileProvider.class);
|
||||
UPConfig upconfig = profileProvider.getConfiguration();
|
||||
String configAttributeGroup = component.getConfig().getFirst("attribute-group");
|
||||
if (!upconfig.getGroups().stream().anyMatch(g -> g.getName().equals(configAttributeGroup))) {
|
||||
throw new ServerErrorException("Endpoint Configuration Error - attribute-group does not exist.", 500);
|
||||
}
|
||||
|
||||
String configAttributeRegex = component.getConfig().getFirst("attribute-regex");
|
||||
Boolean regexIsBlank = configAttributeRegex == null;
|
||||
|
||||
if (!regexIsBlank) {
|
||||
try {
|
||||
Pattern.compile(configAttributeRegex);
|
||||
} catch (Exception e) {
|
||||
throw new ServerErrorException(
|
||||
"Endpoint Configuration Error - attribute-regex is not a valid regex pattern.", 500);
|
||||
}
|
||||
}
|
||||
|
||||
UserModel authUser = auth.getUser();
|
||||
if (!authUser.hasRole(authRole)) {
|
||||
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.");
|
||||
}
|
||||
|
june
commented
Moving this before line 71, where we get the component to work with might make a bit more sense flow-wise. Moving this before line 71, where we get the component to work with might make a bit more sense flow-wise.
|
||||
|
||||
List<String> attributeNames = upconfig.getAttributes()
|
||||
.stream()
|
||||
.filter(a -> a.getGroup() != null && a.getGroup().equals(configAttributeGroup))
|
||||
.map(a -> a.getName())
|
||||
.toList();
|
||||
|
||||
UserProvider userProvider = session.users();
|
||||
Stream<UserModel> users = userProvider.getRoleMembersStream(realm, matchRole);
|
||||
|
||||
List<String> attribute_list = users
|
||||
.map(user -> {
|
||||
Stream<String> attributeStream = attributeNames.stream()
|
||||
.map(attributeName -> user.getAttributeStream(attributeName).toList())
|
||||
.flatMap(Collection::stream);
|
||||
|
||||
return attributeStream
|
||||
.filter(attribute -> !attribute.isEmpty())
|
||||
.toList();
|
||||
})
|
||||
.flatMap(List::stream)
|
||||
.filter(attribute -> {
|
||||
if (regexIsBlank) {
|
||||
return true;
|
||||
}
|
||||
final Pattern pattern = Pattern.compile(configAttributeRegex);
|
||||
final Matcher matcher = pattern.matcher(attribute);
|
||||
return matcher.find();
|
||||
})
|
||||
.toList();
|
||||
|
||||
return Response.ok(attribute_list).build();
|
||||
|
||||
}
|
||||
|
||||
private static Auth getAuth(KeycloakSession session) {
|
||||
AuthResult auth = new AppAuthManager.BearerTokenAuthenticator(session).authenticate();
|
||||
|
||||
if (auth == null) {
|
||||
throw new NotAuthorizedException("Bearer");
|
||||
}
|
||||
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
ClientModel client = auth.getClient();
|
||||
return new Auth(realm, auth.getToken(), auth.getUser(), client, auth.getSession(), false);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package de.ccc.hamburg.keycloak.ssh_key;
|
||||
package de.ccc.hamburg.keycloak.attribute_endpoints;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
|
@ -9,16 +9,16 @@ import org.keycloak.services.resource.RealmResourceProviderFactory;
|
|||
import com.google.auto.service.AutoService;
|
||||
|
||||
@AutoService(RealmResourceProviderFactory.class)
|
||||
public class SSHKeyResourceProviderFactory implements RealmResourceProviderFactory {
|
||||
static final String PROVIDER_ID = "ssh-key-provider";
|
||||
public class AttributeEndpointsResourceProviderFactory implements RealmResourceProviderFactory {
|
||||
static final String PROVIDER_ID = "attribute-endpoints-provider";
|
||||
|
||||
@Override
|
||||
public RealmResourceProvider create(KeycloakSession keycloakSession) {
|
||||
return new SSHKeyResourceProvider(keycloakSession);
|
||||
return new AttributeEndpointsResourceProvider(keycloakSession);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope scope) {
|
||||
public void init(Config.Scope config) {
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
services:
|
||||
keycloak:
|
||||
image: quay.io/keycloak/keycloak:26.4.2
|
||||
image: quay.io/keycloak/keycloak:26.5.3
|
||||
pull_policy: always
|
||||
command: start-dev
|
||||
command: "start-dev --features=declarative-ui"
|
||||
environment:
|
||||
KEYCLOAK_ADMIN: admin
|
||||
KEYCLOAK_ADMIN_PASSWORD: admin
|
||||
|
|
@ -10,4 +10,4 @@ services:
|
|||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./ssh-key-provider/target/ssh-key-provider-1.0-SNAPSHOT.jar:/opt/keycloak/providers/ssh-key-provider.jar
|
||||
- ./attribute-endpoints-provider/target/attribute-endpoints-provider-1.0-SNAPSHOT.jar:/opt/keycloak/providers/attribute-endpoints-provider.jar
|
||||
2667
realm-export.json
Normal file
2667
realm-export.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,117 +0,0 @@
|
|||
package de.ccc.hamburg.keycloak.ssh_key;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.GroupModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserProvider;
|
||||
import org.keycloak.representations.userprofile.config.UPConfig;
|
||||
import org.keycloak.services.managers.AppAuthManager;
|
||||
import org.keycloak.services.managers.AppAuthManager.BearerTokenAuthenticator;
|
||||
import org.keycloak.services.managers.Auth;
|
||||
import org.keycloak.services.managers.AuthenticationManager.AuthResult;
|
||||
import org.keycloak.services.resource.RealmResourceProvider;
|
||||
import org.keycloak.userprofile.UserProfileProvider;
|
||||
|
||||
import jakarta.ws.rs.ForbiddenException;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.NotAuthorizedException;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
|
||||
public class SSHKeyResourceProvider implements RealmResourceProvider {
|
||||
private static final Logger LOG = Logger.getLogger(SSHKeyResourceProvider.class);
|
||||
private final KeycloakSession session;
|
||||
|
||||
// taken from: https://github.com/nemchik/ssh-key-regex
|
||||
private static final Pattern SSH_PUBLIC_KEY = Pattern.compile(
|
||||
"^(?<key>(ssh-dss AAAAB3NzaC1kc3|ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNT|ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzOD|ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1Mj|sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb2|ssh-ed25519 AAAAC3NzaC1lZDI1NTE5|sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29t|ssh-rsa AAAAB3NzaC1yc2)[0-9A-Za-z+/]+[=]{0,3})(\\s.*)?$");
|
||||
|
||||
public SSHKeyResourceProvider(KeycloakSession keycloakSession) {
|
||||
this.session = keycloakSession;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getResource() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("export/{group_id}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response exportKeys(@PathParam("group_id") String groupId) {
|
||||
try {
|
||||
SSHKeyResourceProvider.getAuth(session);
|
||||
} catch (Exception e) {
|
||||
System.err.println(e);
|
||||
return Response.status(401, e.getMessage()).build();
|
||||
}
|
||||
|
||||
UserProvider userProvider = session.users();
|
||||
UserProfileProvider profileProvider = session.getProvider(UserProfileProvider.class);
|
||||
UPConfig upconfig = profileProvider.getConfiguration();
|
||||
List<String> attributeNames = upconfig.getAttributes()
|
||||
.stream()
|
||||
.filter(a -> a.getGroup() != null && a.getGroup().equals("de.ccc.hamburg.keycloak.ssh_key.keys"))
|
||||
.map(a -> a.getName())
|
||||
.toList();
|
||||
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
|
||||
// TODO: add allowlist check
|
||||
GroupModel group = realm.getGroupById(groupId);
|
||||
|
||||
Stream<UserModel> users = userProvider.getGroupMembersStream(realm, group);
|
||||
|
||||
List<String> keys = users
|
||||
.map(user -> {
|
||||
return attributeNames
|
||||
.stream()
|
||||
.map(attributeName -> user.getAttributeStream(attributeName).findFirst())
|
||||
.filter(attribute -> attribute.isPresent())
|
||||
.map(attribute -> attribute.get())
|
||||
.toList();
|
||||
})
|
||||
.flatMap(List::stream)
|
||||
.map(key -> {
|
||||
final Matcher matcher = SSH_PUBLIC_KEY.matcher(key);
|
||||
return matcher.find() ? matcher.group("key") : null;
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
|
||||
return Response.ok(Map.of("keys", keys)).build();
|
||||
|
||||
}
|
||||
|
||||
private static Auth getAuth(KeycloakSession session) {
|
||||
AuthResult auth = new AppAuthManager.BearerTokenAuthenticator(session).authenticate();
|
||||
|
||||
if (auth == null) {
|
||||
throw new NotAuthorizedException("Bearer");
|
||||
} else if (!auth.getToken().getIssuedFor().equals("admin-cli")) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
ClientModel client = auth.getClient();
|
||||
return new Auth(realm, auth.getToken(), auth.getUser(), client, auth.getSession(), false);
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue
Should use plural, like: