Skip to content

Commit

Permalink
#177 Add organizations user events (#185)
Browse files Browse the repository at this point in the history
* #177 Add organizations user events

* #177 Fix PR comments
  • Loading branch information
rtufisi authored Mar 1, 2024
1 parent 33e56ea commit a656022
Show file tree
Hide file tree
Showing 14 changed files with 233 additions and 39 deletions.
29 changes: 5 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ The extensions herein are used in the [Phase Two](https://phasetwo.io) cloud off
- [Entities](#entities)
- [Resources](#resources)
- [Mappers](#mappers)
- [Admin Events](#admin-events)
- [Events](#events)
- [Authentication](#authentication)
- [Invitations](#invitations)
- [IdP Discovery](#idp-discovery)
Expand Down Expand Up @@ -156,6 +156,10 @@ A group of custom REST resources are made available for administrator and custom
- [Bulk Roles](./docs/bulk-roles.md) - support for bulk Roles resources
- Identity Providers - A subset of the Keycloak IdP APIs that allows Organization administrators to manage their own IdP

### Events

For more information you can refer to: [Events](./docs/events.md)

### Mappers

There is currently a single OIDC mapper that adds Organization membership and roles to the token. The format of the addition to the token is
Expand All @@ -173,29 +177,6 @@ You can configure the mapper, by going to **Clients** > ***your-client-name*** >

![mapper](./docs/assets/mapper.png)

### Admin Events

Description of the events associated with the management of organizations:

| Path | Method | Event type | Operation |
|-----------------------------------------------------------------------------|----------|---------------------------|-----------|
| `/auth/realms/:realmId/orgs` | `POST` | ORGANIZATION | CREATE |
| `/auth/realms/:realmId/orgs` | `PUT` | ORGANIZATION | UPDATE |
| `/auth/realms/:realmId/orgs` | `DELETE` | ORGANIZATION | DELETE |
| `/auth/realms/:realmId/orgs/:orgId/members/:userId` | `PUT` | ORGANIZATION_MEMBERSHIP | CREATE |
| `/auth/realms/:realmId/orgs/:orgId/members/:userId` | `DELETE` | ORGANIZATION_MEMBERSHIP | DELETE |
| `/auth/realms/:realmId/orgs/:orgId/roles/:roleName` | `POST` | ORGANIZATION_ROLE | CREATE |
| `/auth/realms/:realmId/orgs/:orgId/roles/:roleName/:roleName` | `DELETE` | ORGANIZATION_ROLE | DELETE |
| `/auth/realms/:realmId/orgs/:orgId/roles/:roleName/:roleName` | `PUT` | ORGANIZATION_ROLE | UPDATE |
| `/auth/realms/:realmId/orgs/users/:userId/orgs/:orgId/roles` | `DELETE` | ORGANIZATION_ROLE_MAPPING | DELETE |
| `/auth/realms/:realmId/orgs/users/:userId/orgs/:orgId/roles` | `PATCH` | ORGANIZATION_ROLE_MAPPING | CREATE |
| `/auth/realms/:realmId/orgs/:orgId/roles/:roleName/users/:userId` | `DELETE` | ORGANIZATION_ROLE_MAPPING | DELETE |
| `/auth/realms/:realmId/orgs/:orgId/roles/:roleName/users/:userId` | `PUT` | ORGANIZATION_ROLE_MAPPING | CREATE |
| `/auth/realms/:realmId/orgs/:orgId/roles/:roleName/users/:userId` | `DELETE` | ORGANIZATION_ROLE_MAPPING | DELETE |
| `/auth/realms/:realmId/orgs/:orgId/invitations/:invitationId` | `POST` | INVITATION | CREATE |
| `/auth/realms/:realmId/orgs/:orgId/invitations/:invitationId/:invitationId` | `DELETE` | INVITATION | DELETE |
| `/auth/realms/:realmId/orgs/:orgId/domains/:domainName/verify` | `POST` | DOMAIN | UPDATE |

### Authentication

#### Invitations
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/events/portal-link-success-event.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
50 changes: 50 additions & 0 deletions docs/events.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Events

### Admin Events

Description of the events associated with the management of organizations:

| Path | Method | Event type | Operation |
|-----------------------------------------------------------------------------|----------|---------------------------|-----------|
| `/auth/realms/:realmId/orgs` | `POST` | ORGANIZATION | CREATE |
| `/auth/realms/:realmId/orgs` | `PUT` | ORGANIZATION | UPDATE |
| `/auth/realms/:realmId/orgs` | `DELETE` | ORGANIZATION | DELETE |
| `/auth/realms/:realmId/orgs/:orgId/members/:userId` | `PUT` | ORGANIZATION_MEMBERSHIP | CREATE |
| `/auth/realms/:realmId/orgs/:orgId/members/:userId` | `DELETE` | ORGANIZATION_MEMBERSHIP | DELETE |
| `/auth/realms/:realmId/orgs/:orgId/roles/:roleName` | `POST` | ORGANIZATION_ROLE | CREATE |
| `/auth/realms/:realmId/orgs/:orgId/roles/:roleName/:roleName` | `DELETE` | ORGANIZATION_ROLE | DELETE |
| `/auth/realms/:realmId/orgs/:orgId/roles/:roleName/:roleName` | `PUT` | ORGANIZATION_ROLE | UPDATE |
| `/auth/realms/:realmId/orgs/users/:userId/orgs/:orgId/roles` | `DELETE` | ORGANIZATION_ROLE_MAPPING | DELETE |
| `/auth/realms/:realmId/orgs/users/:userId/orgs/:orgId/roles` | `PATCH` | ORGANIZATION_ROLE_MAPPING | CREATE |
| `/auth/realms/:realmId/orgs/:orgId/roles/:roleName/users/:userId` | `DELETE` | ORGANIZATION_ROLE_MAPPING | DELETE |
| `/auth/realms/:realmId/orgs/:orgId/roles/:roleName/users/:userId` | `PUT` | ORGANIZATION_ROLE_MAPPING | CREATE |
| `/auth/realms/:realmId/orgs/:orgId/roles/:roleName/users/:userId` | `DELETE` | ORGANIZATION_ROLE_MAPPING | DELETE |
| `/auth/realms/:realmId/orgs/:orgId/invitations/:invitationId` | `POST` | INVITATION | CREATE |
| `/auth/realms/:realmId/orgs/:orgId/invitations/:invitationId/:invitationId` | `DELETE` | INVITATION | DELETE |
| `/auth/realms/:realmId/orgs/:orgId/domains/:domainName/verify` | `POST` | DOMAIN | UPDATE |

### User events

Description of the events associated with users in the context of a organization.

Organization management

| Path | Method | Event type |
|-----------------------------------------------------|----------|-----------------|
| `/auth/realms/:realmId/users/switch-organization` | `PUT` | UPDATE_PROFILE |
| `/auth/realms/:realmId/orgs/:orgId/members/:userId` | `DELETE` | UPDATE_PROFILE |

`Invitations` - Event type: CUSTOM_REQUIRED_ACTION

![invitation-required-action-success](./assets/events/invitation-required-action-success-event.png)

![invitation-required-action-error](./assets/events/invitation-required-action-error-event.png)

`Post IdP login ("Add user to org" Authenticator)` - Event type: IDENTITY_PROVIDER_POST_LOGIN

![add-to-organization-success](./assets/events/add-to-organization-success-event.png)


`PortalLink` - Event type: EXECUTE_ACTION_TOKEN

![portal-link-success](./assets/events/portal-link-success-event.png)
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
package io.phasetwo.service.auth;

import static io.phasetwo.service.Orgs.*;

import com.google.auto.service.AutoService;
import io.phasetwo.service.model.OrganizationModel;
import io.phasetwo.service.model.OrganizationProvider;
import java.util.Map;
import lombok.extern.jbosslog.JBossLog;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.Authenticator;
Expand All @@ -16,6 +13,10 @@
import org.keycloak.models.RealmModel;
import org.keycloak.provider.ProviderEvent;

import java.util.Map;

import static io.phasetwo.service.Orgs.ORG_OWNER_CONFIG_KEY;

/** */
@JBossLog
@AutoService(AuthenticatorFactory.class)
Expand Down Expand Up @@ -61,6 +62,10 @@ private void addUser(AuthenticationFlowContext context) {
org.getName(), context.getUser().getUsername());
org.grantMembership(context.getUser());
// TODO default roles from config??
context.getEvent()
.user(context.getUser())
.detail("joined_organization", org.getId())
.success();
}
} else {
log.infof("No organization owns IdP %s", brokerContext.getIdpConfig().getAlias());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ public AuthenticationSessionModel startFreshAuthenticationSession(
@Override
public Response handleToken(
PortalLinkActionToken token, ActionTokenContext<PortalLinkActionToken> tokenContext) {
EventBuilder event = tokenContext.getEvent();
log.infof(
"handleToken for iss:%s, org:%s, user:%s, rdu:%s",
token.getIssuedFor(), token.getOrgId(), token.getUserId(), token.getRedirectUri());
Expand Down Expand Up @@ -85,6 +86,10 @@ public Response handleToken(
// set the orgId to a user session note
authSession.setUserSessionNote(FIELD_ORG_ID, token.getOrgId());

event
.detail(FIELD_ORG_ID, token.getOrgId())
.success();

String nextAction =
AuthenticationManager.nextRequiredAction(
tokenContext.getSession(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,20 @@
import io.phasetwo.service.model.OrganizationRoleModel;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.extern.jbosslog.JBossLog;
import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.keycloak.events.EventType.CUSTOM_REQUIRED_ACTION;

/** */
@JBossLog
public class InvitationRequiredAction implements RequiredActionProvider {
Expand Down Expand Up @@ -69,6 +73,8 @@ public void requiredActionChallenge(RequiredActionContext context) {

@Override
public void processAction(RequiredActionContext context) {
EventBuilder event = context.getEvent();

RealmModel realm = context.getRealm();
UserModel user = context.getUser();
log.infof(
Expand All @@ -87,11 +93,21 @@ public void processAction(RequiredActionContext context) {
// add membership
log.infof("selected %s", i.getOrganization().getId());
memberFromInvitation(i, user);
// todo future tell the inviter they accepted
event.clone()
.event(CUSTOM_REQUIRED_ACTION)
.user(user)
.detail("org_id", i.getOrganization().getId())
.detail("invitation_id", i.getId())
.success();
}
// revoke invitation
i.getOrganization().revokeInvitation(i.getId());
// todo future tell the inviter they rejected
event.clone()
.event(CUSTOM_REQUIRED_ACTION)
.detail("org_id", i.getOrganization().getId())
.detail("invitation_id", i.getId())
.user(user)
.error("User invitation revoked.");
});

context.success();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static io.phasetwo.service.Orgs.ACTIVE_ORGANIZATION;
import static io.phasetwo.service.resource.OrganizationResourceType.*;
import static org.keycloak.events.EventType.UPDATE_PROFILE;
import static org.keycloak.models.utils.ModelToRepresentation.*;

import com.google.common.base.Strings;
Expand All @@ -12,6 +13,7 @@
import jakarta.ws.rs.core.Response;
import java.util.stream.Stream;
import lombok.extern.jbosslog.JBossLog;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.admin.OperationType;
import org.keycloak.models.Constants;
import org.keycloak.models.UserModel;
Expand Down Expand Up @@ -69,6 +71,13 @@ public Response removeMember(@PathParam("userId") String userId) {
if (activeOrganizationUtil.isValid()
&& activeOrganizationUtil.getActiveOrganization().getId().equals(organization.getId())) {
member.removeAttribute(ACTIVE_ORGANIZATION);

EventBuilder event = new EventBuilder(realm, session, connection);
event
.event(UPDATE_PROFILE)
.user(user)
.detail("removed_active_organization_id", activeOrganizationUtil.getActiveOrganization().getId())
.success();
}

organization.revokeMembership(member);
Expand Down
12 changes: 11 additions & 1 deletion src/main/java/io/phasetwo/service/resource/UserResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static io.phasetwo.service.Orgs.ACTIVE_ORGANIZATION;
import static io.phasetwo.service.resource.Converters.*;
import static io.phasetwo.service.resource.OrganizationResourceType.ORGANIZATION_ROLE_MAPPING;
import static org.keycloak.events.EventType.UPDATE_PROFILE;

import io.phasetwo.service.model.OrganizationModel;
import io.phasetwo.service.model.OrganizationRoleModel;
Expand All @@ -21,6 +22,7 @@
import java.util.List;
import java.util.stream.Stream;
import lombok.extern.jbosslog.JBossLog;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.admin.OperationType;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.UserModel;
Expand Down Expand Up @@ -88,9 +90,17 @@ public Response switchActiveOrganization(@Valid SwitchOrganization body) {
}

// attribute based active organization
var currentActiveOrganization = user.getFirstAttribute(ACTIVE_ORGANIZATION);
user.setAttribute(ACTIVE_ORGANIZATION, Collections.singletonList(body.getId()));
TokenManager tokenManager = new TokenManager(session, auth.getToken(), realm, user);

EventBuilder event = new EventBuilder(realm, session, connection);

event
.event(UPDATE_PROFILE)
.user(user)
.detail("new_active_organization_id", body.getId())
.detail("previous_active_organization_id", currentActiveOrganization)
.success();
return Response.ok(tokenManager.generateTokens()).build();
}

Expand Down
13 changes: 8 additions & 5 deletions src/main/java/io/phasetwo/service/util/ActiveOrganization.java
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
package io.phasetwo.service.util;

import static io.phasetwo.service.Orgs.ACTIVE_ORGANIZATION;

import com.google.common.collect.Lists;
import io.phasetwo.service.model.OrganizationModel;
import io.phasetwo.service.model.OrganizationProvider;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;

import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;

import static io.phasetwo.service.Orgs.ACTIVE_ORGANIZATION;

public class ActiveOrganization {

private static final Logger log = Logger.getLogger(ActiveOrganization.class);
Expand Down Expand Up @@ -61,6 +62,8 @@ public boolean isValid() {
if (organization == null) {
log.warnf("organization doesn't exists anymore.");
user.removeAttribute(ACTIVE_ORGANIZATION);
//TODO: This method has a side effect. In the future it should be removed and the code refactored
//Tests failed if the we had event here
}

return false;
Expand Down
5 changes: 5 additions & 0 deletions src/test/java/io/phasetwo/service/Helpers.java
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ public static List<AdminEventRepresentation> getAdminEvents(Keycloak keycloak, S
return realmResource.getAdminEvents();
}

public static void clearEvents(Keycloak keycloak, String realm) {
RealmResource realmResource = keycloak.realm(realm);
realmResource.clearEvents();
}

public static void clearAdminEvents(Keycloak keycloak, String realm) {
RealmResource realmResource = keycloak.realm(realm);
realmResource.clearAdminEvents();
Expand Down
Loading

0 comments on commit a656022

Please sign in to comment.