Skip to content

Commit a656022

Browse files
authored
#177 Add organizations user events (#185)
* #177 Add organizations user events * #177 Fix PR comments
1 parent 33e56ea commit a656022

14 files changed

+233
-39
lines changed

README.md

Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ The extensions herein are used in the [Phase Two](https://phasetwo.io) cloud off
2525
- [Entities](#entities)
2626
- [Resources](#resources)
2727
- [Mappers](#mappers)
28-
- [Admin Events](#admin-events)
28+
- [Events](#events)
2929
- [Authentication](#authentication)
3030
- [Invitations](#invitations)
3131
- [IdP Discovery](#idp-discovery)
@@ -156,6 +156,10 @@ A group of custom REST resources are made available for administrator and custom
156156
- [Bulk Roles](./docs/bulk-roles.md) - support for bulk Roles resources
157157
- Identity Providers - A subset of the Keycloak IdP APIs that allows Organization administrators to manage their own IdP
158158

159+
### Events
160+
161+
For more information you can refer to: [Events](./docs/events.md)
162+
159163
### Mappers
160164

161165
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
@@ -173,29 +177,6 @@ You can configure the mapper, by going to **Clients** > ***your-client-name*** >
173177

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

176-
### Admin Events
177-
178-
Description of the events associated with the management of organizations:
179-
180-
| Path | Method | Event type | Operation |
181-
|-----------------------------------------------------------------------------|----------|---------------------------|-----------|
182-
| `/auth/realms/:realmId/orgs` | `POST` | ORGANIZATION | CREATE |
183-
| `/auth/realms/:realmId/orgs` | `PUT` | ORGANIZATION | UPDATE |
184-
| `/auth/realms/:realmId/orgs` | `DELETE` | ORGANIZATION | DELETE |
185-
| `/auth/realms/:realmId/orgs/:orgId/members/:userId` | `PUT` | ORGANIZATION_MEMBERSHIP | CREATE |
186-
| `/auth/realms/:realmId/orgs/:orgId/members/:userId` | `DELETE` | ORGANIZATION_MEMBERSHIP | DELETE |
187-
| `/auth/realms/:realmId/orgs/:orgId/roles/:roleName` | `POST` | ORGANIZATION_ROLE | CREATE |
188-
| `/auth/realms/:realmId/orgs/:orgId/roles/:roleName/:roleName` | `DELETE` | ORGANIZATION_ROLE | DELETE |
189-
| `/auth/realms/:realmId/orgs/:orgId/roles/:roleName/:roleName` | `PUT` | ORGANIZATION_ROLE | UPDATE |
190-
| `/auth/realms/:realmId/orgs/users/:userId/orgs/:orgId/roles` | `DELETE` | ORGANIZATION_ROLE_MAPPING | DELETE |
191-
| `/auth/realms/:realmId/orgs/users/:userId/orgs/:orgId/roles` | `PATCH` | ORGANIZATION_ROLE_MAPPING | CREATE |
192-
| `/auth/realms/:realmId/orgs/:orgId/roles/:roleName/users/:userId` | `DELETE` | ORGANIZATION_ROLE_MAPPING | DELETE |
193-
| `/auth/realms/:realmId/orgs/:orgId/roles/:roleName/users/:userId` | `PUT` | ORGANIZATION_ROLE_MAPPING | CREATE |
194-
| `/auth/realms/:realmId/orgs/:orgId/roles/:roleName/users/:userId` | `DELETE` | ORGANIZATION_ROLE_MAPPING | DELETE |
195-
| `/auth/realms/:realmId/orgs/:orgId/invitations/:invitationId` | `POST` | INVITATION | CREATE |
196-
| `/auth/realms/:realmId/orgs/:orgId/invitations/:invitationId/:invitationId` | `DELETE` | INVITATION | DELETE |
197-
| `/auth/realms/:realmId/orgs/:orgId/domains/:domainName/verify` | `POST` | DOMAIN | UPDATE |
198-
199180
### Authentication
200181

201182
#### Invitations
Loading
Loading
Loading
54.1 KB
Loading

docs/events.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Events
2+
3+
### Admin Events
4+
5+
Description of the events associated with the management of organizations:
6+
7+
| Path | Method | Event type | Operation |
8+
|-----------------------------------------------------------------------------|----------|---------------------------|-----------|
9+
| `/auth/realms/:realmId/orgs` | `POST` | ORGANIZATION | CREATE |
10+
| `/auth/realms/:realmId/orgs` | `PUT` | ORGANIZATION | UPDATE |
11+
| `/auth/realms/:realmId/orgs` | `DELETE` | ORGANIZATION | DELETE |
12+
| `/auth/realms/:realmId/orgs/:orgId/members/:userId` | `PUT` | ORGANIZATION_MEMBERSHIP | CREATE |
13+
| `/auth/realms/:realmId/orgs/:orgId/members/:userId` | `DELETE` | ORGANIZATION_MEMBERSHIP | DELETE |
14+
| `/auth/realms/:realmId/orgs/:orgId/roles/:roleName` | `POST` | ORGANIZATION_ROLE | CREATE |
15+
| `/auth/realms/:realmId/orgs/:orgId/roles/:roleName/:roleName` | `DELETE` | ORGANIZATION_ROLE | DELETE |
16+
| `/auth/realms/:realmId/orgs/:orgId/roles/:roleName/:roleName` | `PUT` | ORGANIZATION_ROLE | UPDATE |
17+
| `/auth/realms/:realmId/orgs/users/:userId/orgs/:orgId/roles` | `DELETE` | ORGANIZATION_ROLE_MAPPING | DELETE |
18+
| `/auth/realms/:realmId/orgs/users/:userId/orgs/:orgId/roles` | `PATCH` | ORGANIZATION_ROLE_MAPPING | CREATE |
19+
| `/auth/realms/:realmId/orgs/:orgId/roles/:roleName/users/:userId` | `DELETE` | ORGANIZATION_ROLE_MAPPING | DELETE |
20+
| `/auth/realms/:realmId/orgs/:orgId/roles/:roleName/users/:userId` | `PUT` | ORGANIZATION_ROLE_MAPPING | CREATE |
21+
| `/auth/realms/:realmId/orgs/:orgId/roles/:roleName/users/:userId` | `DELETE` | ORGANIZATION_ROLE_MAPPING | DELETE |
22+
| `/auth/realms/:realmId/orgs/:orgId/invitations/:invitationId` | `POST` | INVITATION | CREATE |
23+
| `/auth/realms/:realmId/orgs/:orgId/invitations/:invitationId/:invitationId` | `DELETE` | INVITATION | DELETE |
24+
| `/auth/realms/:realmId/orgs/:orgId/domains/:domainName/verify` | `POST` | DOMAIN | UPDATE |
25+
26+
### User events
27+
28+
Description of the events associated with users in the context of a organization.
29+
30+
Organization management
31+
32+
| Path | Method | Event type |
33+
|-----------------------------------------------------|----------|-----------------|
34+
| `/auth/realms/:realmId/users/switch-organization` | `PUT` | UPDATE_PROFILE |
35+
| `/auth/realms/:realmId/orgs/:orgId/members/:userId` | `DELETE` | UPDATE_PROFILE |
36+
37+
`Invitations` - Event type: CUSTOM_REQUIRED_ACTION
38+
39+
![invitation-required-action-success](./assets/events/invitation-required-action-success-event.png)
40+
41+
![invitation-required-action-error](./assets/events/invitation-required-action-error-event.png)
42+
43+
`Post IdP login ("Add user to org" Authenticator)` - Event type: IDENTITY_PROVIDER_POST_LOGIN
44+
45+
![add-to-organization-success](./assets/events/add-to-organization-success-event.png)
46+
47+
48+
`PortalLink` - Event type: EXECUTE_ACTION_TOKEN
49+
50+
![portal-link-success](./assets/events/portal-link-success-event.png)

src/main/java/io/phasetwo/service/auth/OrgAddUserAuthenticatorFactory.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
package io.phasetwo.service.auth;
22

3-
import static io.phasetwo.service.Orgs.*;
4-
53
import com.google.auto.service.AutoService;
64
import io.phasetwo.service.model.OrganizationModel;
75
import io.phasetwo.service.model.OrganizationProvider;
8-
import java.util.Map;
96
import lombok.extern.jbosslog.JBossLog;
107
import org.keycloak.authentication.AuthenticationFlowContext;
118
import org.keycloak.authentication.Authenticator;
@@ -16,6 +13,10 @@
1613
import org.keycloak.models.RealmModel;
1714
import org.keycloak.provider.ProviderEvent;
1815

16+
import java.util.Map;
17+
18+
import static io.phasetwo.service.Orgs.ORG_OWNER_CONFIG_KEY;
19+
1920
/** */
2021
@JBossLog
2122
@AutoService(AuthenticatorFactory.class)
@@ -61,6 +62,10 @@ private void addUser(AuthenticationFlowContext context) {
6162
org.getName(), context.getUser().getUsername());
6263
org.grantMembership(context.getUser());
6364
// TODO default roles from config??
65+
context.getEvent()
66+
.user(context.getUser())
67+
.detail("joined_organization", org.getId())
68+
.success();
6469
}
6570
} else {
6671
log.infof("No organization owns IdP %s", brokerContext.getIdpConfig().getAlias());

src/main/java/io/phasetwo/service/auth/action/PortalLinkActionTokenHandler.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ public AuthenticationSessionModel startFreshAuthenticationSession(
5757
@Override
5858
public Response handleToken(
5959
PortalLinkActionToken token, ActionTokenContext<PortalLinkActionToken> tokenContext) {
60+
EventBuilder event = tokenContext.getEvent();
6061
log.infof(
6162
"handleToken for iss:%s, org:%s, user:%s, rdu:%s",
6263
token.getIssuedFor(), token.getOrgId(), token.getUserId(), token.getRedirectUri());
@@ -85,6 +86,10 @@ public Response handleToken(
8586
// set the orgId to a user session note
8687
authSession.setUserSessionNote(FIELD_ORG_ID, token.getOrgId());
8788

89+
event
90+
.detail(FIELD_ORG_ID, token.getOrgId())
91+
.success();
92+
8893
String nextAction =
8994
AuthenticationManager.nextRequiredAction(
9095
tokenContext.getSession(),

src/main/java/io/phasetwo/service/auth/invitation/InvitationRequiredAction.java

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,20 @@
55
import io.phasetwo.service.model.OrganizationRoleModel;
66
import jakarta.ws.rs.core.MultivaluedMap;
77
import jakarta.ws.rs.core.Response;
8-
import java.util.List;
9-
import java.util.Map;
10-
import java.util.stream.Collectors;
11-
import java.util.stream.Stream;
128
import lombok.extern.jbosslog.JBossLog;
139
import org.keycloak.authentication.RequiredActionContext;
1410
import org.keycloak.authentication.RequiredActionProvider;
11+
import org.keycloak.events.EventBuilder;
1512
import org.keycloak.models.RealmModel;
1613
import org.keycloak.models.UserModel;
1714

15+
import java.util.List;
16+
import java.util.Map;
17+
import java.util.stream.Collectors;
18+
import java.util.stream.Stream;
19+
20+
import static org.keycloak.events.EventType.CUSTOM_REQUIRED_ACTION;
21+
1822
/** */
1923
@JBossLog
2024
public class InvitationRequiredAction implements RequiredActionProvider {
@@ -69,6 +73,8 @@ public void requiredActionChallenge(RequiredActionContext context) {
6973

7074
@Override
7175
public void processAction(RequiredActionContext context) {
76+
EventBuilder event = context.getEvent();
77+
7278
RealmModel realm = context.getRealm();
7379
UserModel user = context.getUser();
7480
log.infof(
@@ -87,11 +93,21 @@ public void processAction(RequiredActionContext context) {
8793
// add membership
8894
log.infof("selected %s", i.getOrganization().getId());
8995
memberFromInvitation(i, user);
90-
// todo future tell the inviter they accepted
96+
event.clone()
97+
.event(CUSTOM_REQUIRED_ACTION)
98+
.user(user)
99+
.detail("org_id", i.getOrganization().getId())
100+
.detail("invitation_id", i.getId())
101+
.success();
91102
}
92103
// revoke invitation
93104
i.getOrganization().revokeInvitation(i.getId());
94-
// todo future tell the inviter they rejected
105+
event.clone()
106+
.event(CUSTOM_REQUIRED_ACTION)
107+
.detail("org_id", i.getOrganization().getId())
108+
.detail("invitation_id", i.getId())
109+
.user(user)
110+
.error("User invitation revoked.");
95111
});
96112

97113
context.success();

src/main/java/io/phasetwo/service/resource/MembersResource.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import static io.phasetwo.service.Orgs.ACTIVE_ORGANIZATION;
44
import static io.phasetwo.service.resource.OrganizationResourceType.*;
5+
import static org.keycloak.events.EventType.UPDATE_PROFILE;
56
import static org.keycloak.models.utils.ModelToRepresentation.*;
67

78
import com.google.common.base.Strings;
@@ -12,6 +13,7 @@
1213
import jakarta.ws.rs.core.Response;
1314
import java.util.stream.Stream;
1415
import lombok.extern.jbosslog.JBossLog;
16+
import org.keycloak.events.EventBuilder;
1517
import org.keycloak.events.admin.OperationType;
1618
import org.keycloak.models.Constants;
1719
import org.keycloak.models.UserModel;
@@ -69,6 +71,13 @@ public Response removeMember(@PathParam("userId") String userId) {
6971
if (activeOrganizationUtil.isValid()
7072
&& activeOrganizationUtil.getActiveOrganization().getId().equals(organization.getId())) {
7173
member.removeAttribute(ACTIVE_ORGANIZATION);
74+
75+
EventBuilder event = new EventBuilder(realm, session, connection);
76+
event
77+
.event(UPDATE_PROFILE)
78+
.user(user)
79+
.detail("removed_active_organization_id", activeOrganizationUtil.getActiveOrganization().getId())
80+
.success();
7281
}
7382

7483
organization.revokeMembership(member);

src/main/java/io/phasetwo/service/resource/UserResource.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import static io.phasetwo.service.Orgs.ACTIVE_ORGANIZATION;
44
import static io.phasetwo.service.resource.Converters.*;
55
import static io.phasetwo.service.resource.OrganizationResourceType.ORGANIZATION_ROLE_MAPPING;
6+
import static org.keycloak.events.EventType.UPDATE_PROFILE;
67

78
import io.phasetwo.service.model.OrganizationModel;
89
import io.phasetwo.service.model.OrganizationRoleModel;
@@ -21,6 +22,7 @@
2122
import java.util.List;
2223
import java.util.stream.Stream;
2324
import lombok.extern.jbosslog.JBossLog;
25+
import org.keycloak.events.EventBuilder;
2426
import org.keycloak.events.admin.OperationType;
2527
import org.keycloak.models.KeycloakSession;
2628
import org.keycloak.models.UserModel;
@@ -88,9 +90,17 @@ public Response switchActiveOrganization(@Valid SwitchOrganization body) {
8890
}
8991

9092
// attribute based active organization
93+
var currentActiveOrganization = user.getFirstAttribute(ACTIVE_ORGANIZATION);
9194
user.setAttribute(ACTIVE_ORGANIZATION, Collections.singletonList(body.getId()));
9295
TokenManager tokenManager = new TokenManager(session, auth.getToken(), realm, user);
93-
96+
EventBuilder event = new EventBuilder(realm, session, connection);
97+
98+
event
99+
.event(UPDATE_PROFILE)
100+
.user(user)
101+
.detail("new_active_organization_id", body.getId())
102+
.detail("previous_active_organization_id", currentActiveOrganization)
103+
.success();
94104
return Response.ok(tokenManager.generateTokens()).build();
95105
}
96106

src/main/java/io/phasetwo/service/util/ActiveOrganization.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
package io.phasetwo.service.util;
22

3-
import static io.phasetwo.service.Orgs.ACTIVE_ORGANIZATION;
4-
53
import com.google.common.collect.Lists;
64
import io.phasetwo.service.model.OrganizationModel;
75
import io.phasetwo.service.model.OrganizationProvider;
8-
import java.util.List;
9-
import java.util.Optional;
10-
import java.util.stream.Stream;
116
import org.jboss.logging.Logger;
127
import org.keycloak.models.KeycloakSession;
138
import org.keycloak.models.RealmModel;
149
import org.keycloak.models.UserModel;
1510

11+
import java.util.List;
12+
import java.util.Optional;
13+
import java.util.stream.Stream;
14+
15+
import static io.phasetwo.service.Orgs.ACTIVE_ORGANIZATION;
16+
1617
public class ActiveOrganization {
1718

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

6669
return false;

src/test/java/io/phasetwo/service/Helpers.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ public static List<AdminEventRepresentation> getAdminEvents(Keycloak keycloak, S
116116
return realmResource.getAdminEvents();
117117
}
118118

119+
public static void clearEvents(Keycloak keycloak, String realm) {
120+
RealmResource realmResource = keycloak.realm(realm);
121+
realmResource.clearEvents();
122+
}
123+
119124
public static void clearAdminEvents(Keycloak keycloak, String realm) {
120125
RealmResource realmResource = keycloak.realm(realm);
121126
realmResource.clearAdminEvents();

0 commit comments

Comments
 (0)