Skip to content

[feature/ha-support] Support for Postgres HA #803

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import io.featurehub.db.publish.CacheSource;
import io.featurehub.db.publish.MRPublishModule;
import io.featurehub.db.publish.PublishManager;
import io.featurehub.db.services.InternalOAuthPersonCreation;
import io.featurehub.db.services.OAuthPersonCreation;
import io.featurehub.db.utils.ApiToSqlApiBinder;
import io.featurehub.db.utils.ComplexUpdateMigrations;
import io.featurehub.mr.api.ApplicationServiceDelegate;
Expand Down Expand Up @@ -97,6 +99,7 @@ public boolean configure(FeatureContext context) {
@Override
protected void configure() {
bind(OAuth2MRAdapter.class).to(SSOCompletionListener.class).in(Singleton.class);
bind(OAuthPersonCreation.class).to(InternalOAuthPersonCreation.class).in(Singleton.class);
bind(DatabaseAuthRepository.class).to(AuthenticationRepository.class).in(Singleton.class);
bind(PortfolioUtils.class).to(PortfolioUtils.class).in(Singleton.class);
bind(AuthManager.class).to(AuthManagerService.class).in(Singleton.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,13 @@
import cd.connect.app.config.ConfigKey;
import cd.connect.app.config.DeclaredConfigResolver;
import io.featurehub.db.api.AuthenticationApi;
import io.featurehub.db.api.GroupApi;
import io.featurehub.db.api.OptimisticLockingException;
import io.featurehub.db.api.Opts;
import io.featurehub.db.api.OrganizationApi;
import io.featurehub.db.api.PersonApi;
import io.featurehub.db.api.PortfolioApi;
import io.featurehub.db.services.InternalOAuthPersonCreation;
import io.featurehub.mr.auth.AuthenticationRepository;
import io.featurehub.mr.model.Group;
import io.featurehub.mr.model.Organization;
import io.featurehub.mr.model.Person;
import io.featurehub.mr.model.Portfolio;
import io.featurehub.mr.model.SortOrder;
import io.featurehub.mr.utils.PortfolioUtils;
import io.featurehub.web.security.oauth.SSOCompletionListener;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
Expand All @@ -37,11 +31,9 @@ public class OAuth2MRAdapter implements SSOCompletionListener {

protected final PersonApi personApi;
private final AuthenticationApi authenticationApi;
private final PortfolioApi portfolioApi;
private final GroupApi groupApi;
private final AuthenticationRepository authRepository;
private final PortfolioUtils portfolioUtils;
private final OrganizationApi organizationApi;
private final InternalOAuthPersonCreation internalOAuthPersonCreation;

@ConfigKey("oauth2.cookie.domain")
String cookieDomain = "";
Expand All @@ -50,23 +42,35 @@ public class OAuth2MRAdapter implements SSOCompletionListener {
Boolean cookieSecure = Boolean.FALSE;

@Inject
public OAuth2MRAdapter(PersonApi personApi, AuthenticationApi authenticationApi, PortfolioApi portfolioApi,
GroupApi groupApi, AuthenticationRepository authRepository, PortfolioUtils portfolioUtils, OrganizationApi organizationApi) {
public OAuth2MRAdapter(
PersonApi personApi,
AuthenticationApi authenticationApi,
AuthenticationRepository authRepository,
OrganizationApi organizationApi,
InternalOAuthPersonCreation internalOAuthPersonCreation) {
this.personApi = personApi;
this.authenticationApi = authenticationApi;
this.portfolioApi = portfolioApi;
this.groupApi = groupApi;
this.authRepository = authRepository;
this.portfolioUtils = portfolioUtils;
this.organizationApi = organizationApi;
this.internalOAuthPersonCreation = internalOAuthPersonCreation;

DeclaredConfigResolver.resolve(this);
}

@NotNull
@Override
public Response successfulCompletion(@Nullable String email, @Nullable String username, boolean userMustBeCreatedFirst,
@Nullable String failureUrl, @Nullable String successUrl, @NotNull String provider) {
public Response successfulCompletion(
@Nullable String email,
@Nullable String username,
boolean userMustBeCreatedFirst,
@Nullable String failureUrl,
@Nullable String successUrl,
@NotNull String provider) {
if (email == null || username == null) {
log.warn("User tried to login via SSO with no email `` or username ``", email, username);

return Response.status(302).location(URI.create(failureUrl)).build();
}
// discover if they are a user and if not, add them
Person p = personApi.get(email, Opts.empty());

Expand All @@ -76,18 +80,22 @@ public Response successfulCompletion(@Nullable String email, @Nullable String us
}

if (p == null) {
// if the user must be created in the database before they are allowed to sign in, redirect to failure.
// if the user must be created in the database before they are allowed to sign in, redirect to
// failure.
if (userMustBeCreatedFirst) {
log.warn("User {} attempted to login and they aren't in the database and they need to be.", email);
log.warn(
"User {} attempted to login and they aren't in the database and they need to be.",
email);
return Response.status(302).location(URI.create(failureUrl)).build();
}

p = createUser(email, username);
p = internalOAuthPersonCreation.createUser(email, username);
} else {
p.setName(username);

try {
p.setGroups(null); // don't update groups.
// this is our single write, all in one transaction so we should be good for HA
personApi.update(p.getId().getId(), p, Opts.empty(), p.getId().getId());
} catch (OptimisticLockingException ignored) {
}
Expand All @@ -100,50 +108,21 @@ public Response successfulCompletion(@Nullable String email, @Nullable String us

URI uri = URI.create(successUrl);
// add cookie
return Response.status(Response.Status.FOUND).cookie(
new NewCookie("bearer-token", token, "/",
cookieDomain.isEmpty() ? null : cookieDomain, DEFAULT_VERSION, null,
DEFAULT_MAX_AGE, null, cookieSecure,
false))
.location(uri).build();
}

private Person createUser(String email, String username) {
// determine if they were the first user, and if so, complete setup
boolean firstUser = personApi.noUsersExist();

// first we create them, this will give them a token and so forth, we are playing with existing functionality
// here
try {
personApi.create(email, username,null);
} catch (PersonApi.DuplicatePersonException e) {
log.error("Shouldn't get here, as we check if the person exists before creating them.");
return null;
}

// now "register" them. We can provide a null password OK, it just ignores it, but this removes
// any registration token required
Person person = authenticationApi.register(username, email, null, null);

if (firstUser) {
Organization organization = organizationApi.get();
// create the superuser group and add admin to the group -
Group group = groupApi.createOrgAdminGroup(organization.getId(), "org_admin", person);
groupApi.addPersonToGroup(group.getId(), person.getId().getId(), Opts.empty());

// find the only portfolio and update its members to include this one
final Portfolio portfolio = portfolioApi.findPortfolios(null, SortOrder.ASC,
Opts.empty(), person).get(0);

try {
groupApi.createPortfolioGroup(portfolio.getId(),
new Group().name(portfolioUtils.formatPortfolioAdminGroupName(portfolio)).admin(true), person);
} catch (GroupApi.DuplicateGroupException e) {
log.error("If we have this exception, the site is broken.", e);
}
}

return person;
return Response.status(Response.Status.FOUND)
.cookie(
new NewCookie(
"bearer-token",
token,
"/",
cookieDomain.isEmpty() ? null : cookieDomain,
DEFAULT_VERSION,
null,
DEFAULT_MAX_AGE,
null,
cookieSecure,
false))
.location(uri)
.build();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.featurehub.mr.utils;

import cd.connect.app.config.ConfigKey;
import cd.connect.app.config.DeclaredConfigResolver;
import io.featurehub.mr.model.Portfolio;
import jakarta.inject.Singleton;

Expand All @@ -10,6 +11,10 @@ public class PortfolioUtils {
@ConfigKey("portfolio.admin.group.suffix")
private String portfolioAdminGroupSuffix = "Administrators";

public PortfolioUtils() {
DeclaredConfigResolver.resolve(this);
}

public String formatPortfolioAdminGroupName(Portfolio pf) {
return String.format("%s %s", pf.getName(), portfolioAdminGroupSuffix);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package io.featurehub.db.services

import io.ebean.annotation.Transactional
import io.featurehub.db.api.*
import io.featurehub.db.api.GroupApi.DuplicateGroupException
import io.featurehub.db.api.PersonApi.DuplicatePersonException
import io.featurehub.mr.model.*
import io.featurehub.mr.utils.PortfolioUtils
import jakarta.inject.Inject
import org.slf4j.Logger
import org.slf4j.LoggerFactory

/**
* This is in this package so ebean can use it to scan it for transactions. We need to wrap a transaction
* around the whole process so it works as a unit and it ensures the same connection is used via database
* access so we don't get requests split across HA.
*/

interface InternalOAuthPersonCreation {
fun createUser(email: String, username: String): Person?
}

class OAuthPersonCreation @Inject constructor(
private val personApi: PersonApi,
private val authenticationApi: AuthenticationApi,
private val organizationApi: OrganizationApi,
private val groupApi: GroupApi,
private val portfolioApi: PortfolioApi,
private val portfolioUtils: PortfolioUtils
) : InternalOAuthPersonCreation {
private val log: Logger = LoggerFactory.getLogger(OAuthPersonCreation::class.java)

@Transactional
override fun createUser(email: String, username: String): Person? {
// determine if they were the first user, and if so, complete setup
val firstUser: Boolean = personApi.noUsersExist()

// first we create them, this will give them a token and so forth, we are playing with existing functionality
// here
try {
personApi.create(email, username, null)
} catch (e: DuplicatePersonException) {
log.error("Shouldn't get here, as we check if the person exists before creating them.")
return null
}

// now "register" them. We can provide a null password OK, it just ignores it, but this removes
// any registration token required
val person: Person = authenticationApi.register(username, email, null, null)
if (firstUser) {
val organization: Organization = organizationApi.get()
// create the superuser group and add admin to the group -
val group: Group = groupApi.createOrgAdminGroup(organization.id, "org_admin", person)
groupApi.addPersonToGroup(group.id, person.id!!.id, Opts.empty())

// find the only portfolio and update its members to include this one
val portfolio: Portfolio = portfolioApi.findPortfolios(
null, SortOrder.ASC,
Opts.empty(), person
).first()

try {
groupApi.createPortfolioGroup(
portfolio.id,
Group().name(portfolioUtils.formatPortfolioAdminGroupName(portfolio)).admin(true), person
)
} catch (e: DuplicateGroupException) {
log.error("If we have this exception, the site is broken.", e)
}
}
return person
}
}