Skip to content
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

feat(central): remove and enable/disable users via admin ui #4167

Merged
merged 20 commits into from
Oct 9, 2024
Merged
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
115 changes: 113 additions & 2 deletions apps/central/src/components/admin/ManageUsers.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,47 @@
<ButtonAction @click="alterUser" class="mt-0">Update user</ButtonAction>
</form>
<h2>User list</h2>
<TableSimple class="bg-white" :rows="users" :columns="['email']" />
<TableSimple class="bg-white" :rows="users" :columns="['email', 'enabled']">
<template v-slot:rowheader="row">
<template
v-if="row.row.email !== 'admin' && row.row.email !== 'anonymous'"
>
<IconDanger
icon="trash"
@click="
userToDelete = row.row.email;
isModalShown = true;
"
:tooltip="`Delete ${row.row.email}`"
/>
<IconAction
v-if="row.row.enabled"
icon="user-check"
@click="disableUser(row.row.email)"
:tooltip="`disable ${row.row.email}`"
/>
<IconAction
v-else
icon="user-slash"
@click="enableUser(row.row.email)"
:tooltip="`re-enable ${row.row.email}`"
/>
</template>
</template>
</TableSimple>
<ConfirmModal
v-if="isModalShown"
title="Delete User"
:actionLabel="'Delete ' + userToDelete"
actionType="danger"
@close="isModalShown = false"
@confirmed="
removeUser(userToDelete);
userToDelete = '';
isModalShown = false;
"
/>

<Pagination
v-model="page"
:count="userCount"
Expand All @@ -39,6 +79,9 @@ import {
InputString,
InputPassword,
ButtonAction,
IconDanger,
IconAction,
ConfirmModal,
} from "molgenis-components";

export default {
Expand All @@ -51,6 +94,9 @@ export default {
InputString,
InputPassword,
ButtonAction,
IconDanger,
IconAction,
ConfirmModal,
},
props: {
session: {
Expand All @@ -72,6 +118,8 @@ export default {
alterSuccess: null,
alterLoading: false,
showSigninForm: true,
isModalShown: false,
userToDelete: "",
};
},
computed: {
Expand Down Expand Up @@ -111,7 +159,7 @@ export default {
this.loading = true;
request(
"graphql",
`{_admin{users(limit:${this.limit},offset:${this.offset}){email},userCount}}`
`{_admin{users(limit:${this.limit},offset:${this.offset}){email, enabled},userCount}}`
)
.then((data) => {
this.users = data._admin.users;
Expand All @@ -124,6 +172,69 @@ export default {
"internal error or permission denied. Did you log in as admin?";
});
},
removeUser(user) {
this.alterError = null;
this.alterLoading = true;
request(
"graphql",
`mutation{removeUser(email: "${user}"){status,message}}`
)
.then((data) => {
if (data.removeUser.status === "SUCCESS") {
this.alterSuccess = "Success. removed user: " + user;
this.getUserList();
} else {
this.alterError =
"Delete user failed: " + data.changePassword.message;
}
})
.catch((error) => {
this.alterError = "Delete user failed: " + error.response.message;
});
this.alterLoading = false;
},
enableUser(user) {
this.alterError = null;
this.alterLoading = true;
request(
"graphql",
`mutation{setEnabledUser(email: "${user}", enabled:true){status,message}}`
)
.then((data) => {
if (data.setEnabledUser.status === "SUCCESS") {
this.alterSuccess = "Success. enabled user: " + user;
this.getUserList();
} else {
this.alterError =
"Enable user failed: " + data.changePassword.message;
}
})
.catch((error) => {
this.alterError = "Enable user failed: " + error.response.message;
});
this.alterLoading = false;
},
disableUser(user) {
this.alterError = null;
this.alterLoading = true;
request(
"graphql",
`mutation{setEnabledUser(email: "${user}", enabled:false){status,message}}`
)
.then((data) => {
if (data.setEnabledUser.status === "SUCCESS") {
this.alterSuccess = "Success. disabled user: " + user;
this.getUserList();
} else {
this.alterError =
"Disable user failed: " + data.changePassword.message;
}
})
.catch((error) => {
this.alterError = "Disable user failed: " + error.response.message;
});
this.alterLoading = false;
},
},
watch: {
page() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import static org.molgenis.emx2.Constants.MOLGENIS_JWT_SHARED_SECRET;
import static org.molgenis.emx2.Constants.SETTINGS;
import static org.molgenis.emx2.graphql.GraphqlApiMutationResult.Status.SUCCESS;
import static org.molgenis.emx2.graphql.GraphqlApiMutationResult.typeForMutationResult;
import static org.molgenis.emx2.graphql.GraphqlConstants.*;
import static org.molgenis.emx2.graphql.GraphqlSchemaFieldFactory.outputSettingsType;

Expand All @@ -27,6 +29,11 @@ private GraphlAdminFieldFactory() {
.name(EMAIL)
.type(Scalars.GraphQLString)
.build())
.field(
GraphQLFieldDefinition.newFieldDefinition()
.name(ENABLED)
.type(Scalars.GraphQLBoolean)
.build())
.field(
GraphQLFieldDefinition.newFieldDefinition()
.name(SETTINGS)
Expand Down Expand Up @@ -89,9 +96,41 @@ private static Object getUsers(SelectedField selectedField, Database db) {
}
}

public static GraphQLFieldDefinition removeUser(Database database) {
return GraphQLFieldDefinition.newFieldDefinition()
.name("removeUser")
.type(typeForMutationResult)
.argument(GraphQLArgument.newArgument().name(EMAIL).type(Scalars.GraphQLString))
.dataFetcher(
dataFetchingEnvironment -> {
String email = dataFetchingEnvironment.getArgument(EMAIL);
database.removeUser(email);
return new GraphqlApiMutationResult(SUCCESS, "User %s removed", email);
})
.build();
}

public static GraphQLFieldDefinition setEnabledUser(Database database) {
return GraphQLFieldDefinition.newFieldDefinition()
.name("setEnabledUser")
.type(typeForMutationResult)
.argument(GraphQLArgument.newArgument().name(EMAIL).type(Scalars.GraphQLString))
.argument(GraphQLArgument.newArgument().name(ENABLED).type(Scalars.GraphQLBoolean))
.dataFetcher(
dataFetchingEnvironment -> {
String email = dataFetchingEnvironment.getArgument(EMAIL);
Boolean enabled = dataFetchingEnvironment.getArgument(ENABLED);
database.setEnabledUser(email, enabled);
return new GraphqlApiMutationResult(
SUCCESS, "User %s %s ", email, enabled ? "Enabled" : "Disabled");
})
.build();
}

private static Map<String, Object> toGraphqlUser(User user) {
Map<String, Object> result = new LinkedHashMap<>();
result.put(EMAIL, user.getUsername());
result.put(ENABLED, user.getEnabled());
result.put(SETTINGS, mapSettingsToGraphql(user.getSettings()));
return result;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ public GraphQL createGraphqlForDatabase(Database database, TaskService taskServi
// admin operations
if (database.isAdmin()) {
queryBuilder.field(GraphlAdminFieldFactory.queryAdminField(database));
mutationBuilder.field(GraphlAdminFieldFactory.removeUser(database));
mutationBuilder.field(GraphlAdminFieldFactory.setEnabledUser(database));
}

// database operations
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public class GraphqlConstants {
public static final String FILTER_ARGUMENT = "filter";
public static final String KEY = "key";
public static final String PASSWORD = "password";
public static final String ENABLED = "enabled";
public static final String EMAIL = "email";
public static final String NAME = "name";
public static final String LOCALE = "locale";
Expand Down Expand Up @@ -51,6 +52,7 @@ public class GraphqlConstants {
public static final String TASK_DESCRIPTION = "description";
public static final String TASK_SUBTASKS = "subTasks";
public static final String TOKEN_NAME = "tokenName";
public static final String USER = "user";
public static final String USERS = "users";
public static final String ROLES = "roles";
public static final String MESSAGE = "message";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,16 +86,20 @@ public GraphQLFieldDefinition signinField(Database database) {
dataFetchingEnvironment -> {
String userName = dataFetchingEnvironment.getArgument(EMAIL);
String passWord = dataFetchingEnvironment.getArgument(PASSWORD);

if (database.hasUser(userName) && database.checkUserPassword(userName, passWord)) {
database.setActiveUser(userName);
GraphqlApiMutationResultWithToken result =
new GraphqlApiMutationResultWithToken(
GraphqlApiMutationResult.Status.SUCCESS,
JWTgenerator.createTemporaryToken(database, userName),
"Signed in as '%s'",
userName);
return result;
if (database.getUser(userName).getEnabled()) {
database.setActiveUser(userName);
GraphqlApiMutationResultWithToken result =
new GraphqlApiMutationResultWithToken(
GraphqlApiMutationResult.Status.SUCCESS,
JWTgenerator.createTemporaryToken(database, userName),
"Signed in as '%s'",
userName);
return result;
} else {
return new GraphqlApiMutationResult(
FAILED, "User '%s' disabled: check with your administrator", userName);
}
} else {
return new GraphqlApiMutationResult(
FAILED, "Sign in as '%s' failed: user or password unknown", userName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public class MetadataUtils {
private static final org.jooq.Table SCHEMA_METADATA = table(name(MOLGENIS, "schema_metadata"));
private static final org.jooq.Table TABLE_METADATA = table(name(MOLGENIS, "table_metadata"));
private static final org.jooq.Table COLUMN_METADATA = table(name(MOLGENIS, "column_metadata"));
private static final org.jooq.Table USERS_METADATA = table(name(MOLGENIS, "users_metadata"));
public static final org.jooq.Table USERS_METADATA = table(name(MOLGENIS, "users_metadata"));
private static final org.jooq.Table SETTINGS_METADATA =
table(name(MOLGENIS, "settings_metadata"));

Expand Down Expand Up @@ -98,8 +98,9 @@ public class MetadataUtils {
field(name("defaultValue"), VARCHAR.nullable(true));

// users
private static final Field<String> USER_NAME = field(name("username"), VARCHAR);
public static final Field<String> USER_NAME = field(name("username"), VARCHAR);
private static final Field<String> USER_PASS = field(name("password"), VARCHAR);
public static final Field<Boolean> USER_ENABLED = field(name("enabled"), BOOLEAN.nullable(false));

// settings field, reused by all other metadata
static final org.jooq.Field SETTINGS = field(name(org.molgenis.emx2.Constants.SETTINGS), JSON);
Expand Down Expand Up @@ -256,7 +257,9 @@ protected static void init(DSLContext j) {
}

t = jooq.createTableIfNotExists(USERS_METADATA);
t.columns(USER_NAME, USER_PASS).constraint(primaryKey(USER_NAME)).execute();
t.columns(USER_NAME, USER_PASS, USER_ENABLED)
.constraint(primaryKey(USER_NAME))
.execute();

t = jooq.createTableIfNotExists(SETTINGS_METADATA);
t.columns(TABLE_SCHEMA, SETTINGS_TABLE_NAME, SETTINGS_NAME, SETTINGS_VALUE)
Expand Down Expand Up @@ -389,13 +392,15 @@ protected static List<User> loadUsers(SqlDatabase db, int limit, int offset) {
List<User> users = new ArrayList<>();
for (org.jooq.Record user :
db.getJooq()
.select(USER_NAME, SETTINGS)
.select(USER_NAME, USER_ENABLED, SETTINGS)
.from(USERS_METADATA)
.orderBy(USER_NAME)
.limit(limit)
.offset(offset)
.fetchArray()) {
users.add(new User(db, user.get(USER_NAME), user.get(SETTINGS, Map.class)));
User newUser = new User(db, user.get(USER_NAME), user.get(SETTINGS, Map.class));
newUser.setEnabled(user.get(USER_ENABLED));
users.add(newUser);
}
return users;
} catch (Exception e) {
Expand Down Expand Up @@ -602,10 +607,12 @@ private static Column recordToColumn(org.jooq.Record col) {
}

public static void setUserPassword(DSLContext jooq, String user, String password) {
// TODO BEFORE MERGE: set USER_ACTIVE to current value and not to "TRUE"
jooq.insertInto(USERS_METADATA)
.columns(USER_NAME, USER_PASS)
.columns(USER_NAME, USER_ENABLED, USER_PASS)
.values(
field("{0}", String.class, user),
field("{0}", Boolean.class, Boolean.TRUE),
field("crypt({0}, gen_salt('bf'))", String.class, password))
.onConflict(USER_NAME)
.doUpdate()
Expand Down Expand Up @@ -663,8 +670,8 @@ public static Map<String, String> loadDatabaseSettings(DSLContext jooq) {
public static void saveUserMetadata(DSLContext jooq, User user) {
// don't update password via this route
jooq.insertInto(USERS_METADATA)
.columns(USER_NAME, SETTINGS)
.values(user.getUsername(), user.getSettings())
.columns(USER_NAME, USER_ENABLED, SETTINGS)
.values(user.getUsername(), true, user.getSettings())
.onConflict(USER_NAME)
.doUpdate()
.set(SETTINGS, user.getSettings())
Expand All @@ -676,6 +683,7 @@ public static User loadUserMetadata(SqlDatabase db, String userName) {
db.getJooq().selectFrom(USERS_METADATA).where(USER_NAME.eq(userName)).fetchOne();
if (userRecord != null) {
User result = new User(db, userName);
result.setEnabled(userRecord.get(USER_ENABLED));
result.setSettings(userRecord.get(SETTINGS, Map.class));
return result;
}
Expand Down
Loading