Skip to content

Commit

Permalink
Merge pull request #1058 from research-software-directory/admin-role-…
Browse files Browse the repository at this point in the history
…in-database

Admin role in database
  • Loading branch information
ewan-escience authored Nov 28, 2023
2 parents da23b6d + 3eb547e commit a3bec85
Show file tree
Hide file tree
Showing 15 changed files with 103 additions and 86 deletions.
7 changes: 0 additions & 7 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,6 @@ RSD_AUTH_URL=http://auth:7000
# if you add the value "LOCAL", then local accounts are enabled, USE THIS FOR TESTING PURPOSES ONLY
RSD_AUTH_PROVIDERS=SURFCONEXT;ORCID;AZURE;LOCAL

# Define a semicolon-separated list of user email addresses (exact match incl. the letter casing) of RSD admins.
# When someome authenticates with an email address in this list,
# they will get a token with as role rsd_admin, meaning they
# have admin rights for all the tables.
# consumed by: authentication
#RSD_ADMIN_EMAIL_LIST=isaacnewton@university-example.org;admin1@example.com

# Define a semicolon-separated list of user email addresses which are allowed to
# login to the RSD. If the variable is left empty, or is not defined, all users
# will be allowed to login.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
// SPDX-FileCopyrightText: 2022 Ewan Cahen (Netherlands eScience Center) <e.cahen@esciencecenter.nl>
// SPDX-FileCopyrightText: 2022 Netherlands eScience Center
// SPDX-FileCopyrightText: 2022 - 2023 Ewan Cahen (Netherlands eScience Center) <e.cahen@esciencecenter.nl>
// SPDX-FileCopyrightText: 2022 - 2023 Netherlands eScience Center
//
// SPDX-License-Identifier: Apache-2.0

package nl.esciencecenter.rsd.authentication;

import java.util.UUID;

public record AccountInfo(UUID account, String name) {
public record AccountInfo(
UUID account,
String name,
boolean isAdmin
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,6 @@

public class Config {

private static final Collection<String> rsdAdmins;

static {
String adminList = System.getenv("RSD_ADMIN_EMAIL_LIST");
rsdAdmins = adminList == null || adminList.isBlank() ? Collections.emptySet() :
Set.of(adminList.split(";"));
}

public static String jwtSigningSecret() {
return System.getenv("PGRST_JWT_SECRET");
}
Expand All @@ -37,10 +29,6 @@ private static Collection<String> rsdAuthProviders() {
.orElse(Collections.emptySet());
}

public static Collection<String> rsdAdmins() {
return rsdAdmins;
}

public static boolean isLocalEnabled() {
return rsdAuthProviders().contains("LOCAL");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// SPDX-FileCopyrightText: 2022 - 2023 Ewan Cahen (Netherlands eScience Center) <e.cahen@esciencecenter.nl>
// SPDX-FileCopyrightText: 2022 - 2023 Netherlands eScience Center
// SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all)
// SPDX-FileCopyrightText: 2022 Ewan Cahen (Netherlands eScience Center) <e.cahen@esciencecenter.nl>
// SPDX-FileCopyrightText: 2022 Netherlands eScience Center
// SPDX-FileCopyrightText: 2022 dv4all
//
// SPDX-License-Identifier: Apache-2.0
Expand All @@ -15,7 +15,6 @@
import java.util.Date;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;

public class JwtCreator {

Expand All @@ -29,12 +28,12 @@ public JwtCreator(String signingSecret) {
this.signingAlgorithm = Algorithm.HMAC256(this.signingSecret);
}

String createUserJwt(UUID account, String name, boolean isAdmin) {
String createUserJwt(AccountInfo accountInfo) {
return JWT.create()
.withClaim("iss", "rsd_auth")
.withClaim("role", isAdmin ? "rsd_admin" : "rsd_user")
.withClaim("account", account.toString())
.withClaim("name", name)
.withClaim("role", accountInfo.isAdmin() ? "rsd_admin" : "rsd_user")
.withClaim("account", accountInfo.account().toString())
.withClaim("name", accountInfo.name())
.withExpiresAt(new Date(System.currentTimeMillis() + ONE_HOUR_IN_MILLISECONDS))
.sign(signingAlgorithm);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2021 - 2022 Ewan Cahen (Netherlands eScience Center) <e.cahen@esciencecenter.nl>
// SPDX-FileCopyrightText: 2021 - 2022 Netherlands eScience Center
// SPDX-FileCopyrightText: 2021 - 2023 Ewan Cahen (Netherlands eScience Center) <e.cahen@esciencecenter.nl>
// SPDX-FileCopyrightText: 2021 - 2023 Netherlands eScience Center
// SPDX-FileCopyrightText: 2022 - 2023 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences
// SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all)
// SPDX-FileCopyrightText: 2022 Matthias Rüster (GFZ) <matthias.ruester@gfz-potsdam.de>
Expand All @@ -23,12 +23,12 @@ public class Main {
public static boolean userIsAllowed(OpenIdInfo info) {
String whitelist = Config.userMailWhitelist();

if (whitelist == null || whitelist.length() == 0) {
if (whitelist == null || whitelist.isEmpty()) {
// allow any user
return true;
}

if (info == null || info.email() == null || info.email().length() == 0) {
if (info == null || info.email() == null || info.email().isEmpty()) {
throw new Error("Unexpected parameters for 'userIsAllowed'");
}

Expand All @@ -46,11 +46,11 @@ public static boolean userIsAllowed(OpenIdInfo info) {
public static boolean userInAaiAllowList(OpenIdInfo info) {
String allowList = Config.helmholtzAaiAllowList();

if (!Config.helmholtzAaiUseAllowList() || allowList == null || allowList.length() == 0) {
if (!Config.helmholtzAaiUseAllowList() || allowList == null || allowList.isEmpty()) {
return false;
}

if (info == null || info.email() == null || info.email().length() == 0) {
if (info == null || info.email() == null || info.email().isEmpty()) {
throw new Error("Unexpected parameters for 'userInAaiAllowList'");
}

Expand Down Expand Up @@ -99,8 +99,7 @@ public static void main(String[] args) {
OpenIdInfo localInfo = new OpenIdInfo(sub, name, email, organisation);

AccountInfo accountInfo = new PostgrestAccount().account(localInfo, OpenidProvider.local);
boolean isAdmin = isAdmin(email);
createAndSetToken(ctx, accountInfo, isAdmin);
createAndSetToken(ctx, accountInfo);
});
}

Expand All @@ -115,9 +114,7 @@ public static void main(String[] args) {
}

AccountInfo accountInfo = new PostgrestAccount().account(surfconextInfo, OpenidProvider.surfconext);
String email = surfconextInfo.email();
boolean isAdmin = isAdmin(email);
createAndSetToken(ctx, accountInfo, isAdmin);
createAndSetToken(ctx, accountInfo);
});
}

Expand All @@ -132,9 +129,7 @@ public static void main(String[] args) {
}

AccountInfo accountInfo = new PostgrestAccount().account(helmholtzInfo, OpenidProvider.helmholtz);
String email = helmholtzInfo.email();
boolean isAdmin = isAdmin(email);
createAndSetToken(ctx, accountInfo, isAdmin);
createAndSetToken(ctx, accountInfo);
});
}

Expand All @@ -145,9 +140,7 @@ public static void main(String[] args) {
OpenIdInfo orcidInfo = new OrcidLogin(code, redirectUrl).openidInfo();

AccountInfo accountInfo = new PostgrestCheckOrcidWhitelistedAccount(new PostgrestAccount()).account(orcidInfo, OpenidProvider.orcid);
String email = orcidInfo.email();
boolean isAdmin = isAdmin(email);
createAndSetToken(ctx, accountInfo, isAdmin);
createAndSetToken(ctx, accountInfo);
});
}

Expand All @@ -157,9 +150,7 @@ public static void main(String[] args) {
String redirectUrl = Config.azureRedirect();
OpenIdInfo azureInfo = new AzureLogin(code, redirectUrl).openidInfo();
AccountInfo accountInfo = new PostgrestAccount().account(azureInfo, OpenidProvider.azure);
String email = azureInfo.email();
boolean isAdmin = isAdmin(email);
createAndSetToken(ctx, accountInfo, isAdmin);
createAndSetToken(ctx, accountInfo);
});
}

Expand Down Expand Up @@ -198,13 +189,9 @@ public static void main(String[] args) {
});
}

static boolean isAdmin(String email) {
return email != null && !email.isBlank() && Config.rsdAdmins().contains(email);
}

static void createAndSetToken(Context ctx, AccountInfo accountInfo, boolean isAdmin) {
static void createAndSetToken(Context ctx, AccountInfo accountInfo) {
JwtCreator jwtCreator = new JwtCreator(Config.jwtSigningSecret());
String token = jwtCreator.createUserJwt(accountInfo.account(), accountInfo.name(), isAdmin);
String token = jwtCreator.createUserJwt(accountInfo);
setJwtCookie(ctx, token);
setRedirectFromCookie(ctx);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,26 +31,43 @@ public AccountInfo account(OpenIdInfo openIdInfo, OpenidProvider provider) throw
String subject = openIdInfo.sub();
String subjectUrlEncoded = URLEncoder.encode(subject, StandardCharsets.UTF_8);
String providerUrlEncoded = URLEncoder.encode(provider.toString(), StandardCharsets.UTF_8);
URI queryUri = URI.create(backendUri + "/login_for_account?select=id,account,name&sub=eq." + subjectUrlEncoded + "&provider=eq." + providerUrlEncoded);

// The following URI sees if the login credentials already are tied to an account.
// If yes, it also, by joining through the account table, looks up if the account is an admin.
URI queryUri = URI.create(backendUri + "/login_for_account?select=id,account_id:account,name,account(admin_account(account_id))&sub=eq." + subjectUrlEncoded + "&provider=eq." + providerUrlEncoded);
JwtCreator jwtCreator = new JwtCreator(Config.jwtSigningSecret());
String token = jwtCreator.createAdminJwt();
String responseLogin = getAsAdmin(queryUri, token);
JsonArray accountsWithSub = JsonParser.parseString(responseLogin).getAsJsonArray();
if (accountsWithSub.size() > 1)
// Because of the UNIQUE(provider, sub) constraint on the login_for_account table, this should never happen
if (accountsWithSub.size() > 1) {
throw new RuntimeException("More than one login for subject " + subject + " exists");
}
// The credentials are already tied to an account, so we update the login credentials with a possibly new name, email, etc.,
// and we return the existing account.
else if (accountsWithSub.size() == 1) {
JsonObject accountInfo = accountsWithSub.get(0).getAsJsonObject();
UUID id = UUID.fromString(accountInfo.getAsJsonPrimitive("id").getAsString());
updateLoginForAccount(id, openIdInfo, token);
UUID account = UUID.fromString(accountInfo.getAsJsonPrimitive("account").getAsString());
UUID account = UUID.fromString(accountInfo.getAsJsonPrimitive("account_id").getAsString());
String name = openIdInfo.name();
return new AccountInfo(account, name);
} else { // create account

boolean isAdmin = accountInfo.getAsJsonObject("account").get("admin_account").isJsonObject()
&&
accountInfo.getAsJsonObject("account").getAsJsonObject("admin_account").get("account_id").isJsonPrimitive()
&&
accountInfo.getAsJsonObject("account").getAsJsonObject("admin_account").getAsJsonPrimitive("account_id").getAsString().equals(account.toString());

return new AccountInfo(account, name, isAdmin);
}
// The login credentials do no exist yet, create a new account and return it.
else {
// create account
URI createAccountEndpoint = URI.create(backendUri + "/account");
String newAccountJson = postJsonAsAdmin(createAccountEndpoint, "{}", token);
String newAccountId = JsonParser.parseString(newAccountJson).getAsJsonArray().get(0).getAsJsonObject().getAsJsonPrimitive("id").getAsString();

// create login for account
// Create login for account, i.e., tie the login credentials to the newly created account.
JsonObject loginForAccountData = new JsonObject();
loginForAccountData.addProperty("account", newAccountId);
loginForAccountData.addProperty("sub", subject);
Expand All @@ -61,7 +78,7 @@ else if (accountsWithSub.size() == 1) {
URI createLoginUri = URI.create(backendUri + "/login_for_account");
postJsonAsAdmin(createLoginUri, loginForAccountData.toString(), token);

return new AccountInfo(UUID.fromString(newAccountId), openIdInfo.name());
return new AccountInfo(UUID.fromString(newAccountId), openIdInfo.name(), false);
}
}

Expand Down
2 changes: 1 addition & 1 deletion database/004-create-relations-for-software.sql
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ CREATE TYPE platform_type AS ENUM (
);

CREATE TABLE repository_url (
software UUID references software (id) PRIMARY KEY,
software UUID REFERENCES software (id) PRIMARY KEY,
url VARCHAR(200) NOT NULL CHECK (url ~ '^https?://'),
code_platform platform_type NOT NULL DEFAULT 'other',
license VARCHAR(200),
Expand Down
5 changes: 5 additions & 0 deletions database/011-create-account-table.sql
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ $$;
CREATE TRIGGER sanitise_update_login_for_account BEFORE UPDATE ON login_for_account FOR EACH ROW EXECUTE PROCEDURE sanitise_update_login_for_account();


CREATE TABLE admin_account (
account_id UUID REFERENCES account (id) PRIMARY KEY
);


CREATE TABLE orcid_whitelist (
orcid VARCHAR(19) PRIMARY KEY CHECK (orcid ~ '^\d{4}-\d{4}-\d{4}-\d{3}[0-9X]$')
Expand Down Expand Up @@ -125,6 +129,7 @@ BEGIN
DELETE FROM invite_maintainer_for_project WHERE created_by = account_id OR claimed_by = account_id;
DELETE FROM invite_maintainer_for_organisation WHERE created_by = account_id OR claimed_by = account_id;
UPDATE organisation SET primary_maintainer = NULL WHERE primary_maintainer = account_id;
DELETE FROM admin_account WHERE admin_account.account_id = delete_account.account_id;
DELETE FROM login_for_account WHERE account = account_id;
DELETE FROM account WHERE id = account_id;
END
Expand Down
7 changes: 7 additions & 0 deletions database/020-row-level-security.sql
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,13 @@ CREATE POLICY admin_all_rights ON login_for_account TO rsd_admin
WITH CHECK (TRUE);


ALTER TABLE admin_account ENABLE ROW LEVEL SECURITY;

CREATE POLICY admin_all_rights ON admin_account TO rsd_admin
USING (TRUE)
WITH CHECK (TRUE);


ALTER TABLE orcid_whitelist ENABLE ROW LEVEL SECURITY;

CREATE POLICY admin_all_rights ON orcid_whitelist TO rsd_admin
Expand Down
1 change: 0 additions & 1 deletion deployment/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ services:
# it uses values from .env file
- POSTGREST_URL
- RSD_AUTH_PROVIDERS
- RSD_ADMIN_EMAIL_LIST
- RSD_AUTH_USER_MAIL_WHITELIST
- SURFCONEXT_CLIENT_ID
- SURFCONEXT_REDIRECT
Expand Down
5 changes: 2 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ version: "3.0"
services:
database:
build: ./database
image: rsd/database:2.1.1
image: rsd/database:2.2.0
ports:
# enable connection from outside (development mode)
- "5432:5432"
Expand Down Expand Up @@ -53,7 +53,7 @@ services:

auth:
build: ./authentication
image: rsd/auth:1.2.3
image: rsd/auth:1.3.0
ports:
- 5005:5005
expose:
Expand All @@ -62,7 +62,6 @@ services:
# it uses values from .env file
- POSTGREST_URL
- RSD_AUTH_PROVIDERS
- RSD_ADMIN_EMAIL_LIST
- RSD_AUTH_USER_MAIL_WHITELIST
- SURFCONEXT_CLIENT_ID
- SURFCONEXT_REDIRECT
Expand Down
23 changes: 12 additions & 11 deletions documentation/docs/03-rsd-instance/01-getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,22 +55,23 @@ At this point you should be able to see RSD instance running. You should also be
The local account login option is only for test purposes. Local accounts do not require a password and are therefore not safe.
:::

## Login as RSD adminstrator
## Log in as RSD administrator

To be able to login as rsd adminstrator you will need to provide the email address of the logged in user in the `RSD_ADMIN_EMAIL_LIST` property of the .env file. In the example below we defined one rsd admin having the email `isaacnewton@university-example.org`. This is the email of SURFconext test account with the username professor3 which has this email address provided in the JWT token that RSD receives from the SURFconext authentication provider.
To be able to log in as RSD administrator, the account id of that account needs to be in the database table `admin_account` first.
To do so, [connect to the database](/rsd-instance/database/#connecting-to-the-database) and execute the following query, changing the value of the UUID:

```env
# Define a semicolon-separated list of user email addresses of RSD admins.
# When someome authenticates with an email address in this list,
# they will get a token with as role rsd_admin, meaning they
# have admin rights for all the tables.
# consumed by: authentication
RSD_ADMIN_EMAIL_LIST=isaacnewton@university-example.org
```sql
INSERT INTO admin_account VALUES ('00000000-0000-0000-0000-000000000000');
```

:::tip
- When you login to RSD as administrator you will see additional "Administration" option in the profile dropdown menu.
- To define admin email for LOCAL account use @example.org email domain. For example for user `Tester` the email should be `tester@example.org`
A user can see their account ID in their user settings page, which they can find under the `My settings` option in the profile dropdown menu.
:::

If that user is already logged in, they need to log out and log in again before they can make use of their admin rights.

:::tip
When you log in to the RSD as administrator, you will see an additional "Administration" option in the profile dropdown menu.
:::

![Login as rsd admin](img/rsd-login-admin.gif)
Expand Down
Loading

0 comments on commit a3bec85

Please sign in to comment.