Skip to content

Commit

Permalink
#180 Assign invitation roles when joining organization (#186)
Browse files Browse the repository at this point in the history
* #180 Assign invitation roles when joining organization

* #180 Revoke invitation after consumed

* #180 Format files
  • Loading branch information
rtufisi authored Mar 1, 2024
1 parent 2dfe9a1 commit a306362
Show file tree
Hide file tree
Showing 9 changed files with 190 additions and 144 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions docs/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ Organization management

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

![remove-organization-invitation-event](./assets/events/remove-organization-invitation-event.png)


`PortalLink` - Event type: EXECUTE_ACTION_TOKEN

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
package io.phasetwo.service.auth;

import static io.phasetwo.service.Orgs.ORG_OWNER_CONFIG_KEY;
import static org.keycloak.events.EventType.IDENTITY_PROVIDER_POST_LOGIN;

import com.google.auto.service.AutoService;
import io.phasetwo.service.model.InvitationModel;
import io.phasetwo.service.model.OrganizationModel;
import io.phasetwo.service.model.OrganizationProvider;
import io.phasetwo.service.model.OrganizationRoleModel;
import java.util.Map;
import lombok.extern.jbosslog.JBossLog;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.Authenticator;
Expand All @@ -11,12 +17,9 @@
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
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,17 +64,48 @@ private void addUser(AuthenticationFlowContext context) {
"granting membership to %s for user %s",
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();
context
.getEvent()
.user(context.getUser())
.detail("joined_organization", org.getId())
.success();
orgs.getUserInvitationsStream(context.getRealm(), context.getUser())
.filter(
invitationModel -> invitationModel.getOrganization().getId().equals(org.getId()))
.forEach(
invitationModel -> {
addRolesFromInvitation(invitationModel, context.getUser());

invitationModel.getOrganization().revokeInvitation(invitationModel.getId());
context
.getEvent()
.clone()
.event(IDENTITY_PROVIDER_POST_LOGIN)
.detail("org_id", invitationModel.getOrganization().getId())
.detail("invitation_id", invitationModel.getId())
.user(context.getUser())
.error("User invitation revoked.");
});
}
} else {
log.infof("No organization owns IdP %s", brokerContext.getIdpConfig().getAlias());
}
}

void addRolesFromInvitation(InvitationModel invitation, UserModel user) {
invitation
.getRoles()
.forEach(
r -> {
OrganizationRoleModel role = invitation.getOrganization().getRoleByName(r);
if (role == null) {
log.debugf("No org role found for invitation role %s. Skipping...", r);
} else {
role.grantRole(user);
}
});
}

@Override
public boolean requiresUser() {
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,7 @@ 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();
event.detail(FIELD_ORG_ID, token.getOrgId()).success();

String nextAction =
AuthenticationManager.nextRequiredAction(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
package io.phasetwo.service.auth.invitation;

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

import io.phasetwo.service.model.InvitationModel;
import io.phasetwo.service.model.OrganizationProvider;
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 @@ -93,21 +92,23 @@ public void processAction(RequiredActionContext context) {
// add membership
log.infof("selected %s", i.getOrganization().getId());
memberFromInvitation(i, user);
event.clone()
.event(CUSTOM_REQUIRED_ACTION)
.user(user)
.detail("org_id", i.getOrganization().getId())
.detail("invitation_id", i.getId())
.success();
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());
event.clone()
.event(CUSTOM_REQUIRED_ACTION)
.detail("org_id", i.getOrganization().getId())
.detail("invitation_id", i.getId())
.user(user)
.error("User invitation revoked.");
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
10 changes: 6 additions & 4 deletions src/main/java/io/phasetwo/service/resource/MembersResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,12 @@ public Response removeMember(@PathParam("userId") String userId) {

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

organization.revokeMembership(member);
Expand Down
10 changes: 5 additions & 5 deletions src/main/java/io/phasetwo/service/resource/UserResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,11 @@ public Response switchActiveOrganization(@Valid SwitchOrganization body) {
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();
.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
16 changes: 8 additions & 8 deletions src/main/java/io/phasetwo/service/util/ActiveOrganization.java
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
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 @@ -62,8 +61,9 @@ 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
// 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
Loading

0 comments on commit a306362

Please sign in to comment.