Skip to content

Commit

Permalink
Merge pull request #3675 from uselagoon/user-last-accessed
Browse files Browse the repository at this point in the history
feat: store user last accessed, and sshkey last used timestamps
  • Loading branch information
tobybellwood authored Jun 30, 2024
2 parents c8418d2 + 828c247 commit ea51af4
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 0 deletions.
21 changes: 21 additions & 0 deletions services/api/database/migrations/20240512000000_sshkeyused.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = async function(knex) {
return knex.schema
.alterTable('ssh_key', (table) => {
table.datetime('last_used');
})
};

/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = async function(knex) {
return knex.schema
.alterTable('ssh_key', (table) => {
table.dropColumn('last_used');
})
};
34 changes: 34 additions & 0 deletions services/api/src/models/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface User {
firstName?: string;
lastName?: string;
comment?: string;
lastAccessed?: string;
gitlabId?: string;
attributes?: IUserAttributes;
owner?: boolean;
Expand Down Expand Up @@ -55,6 +56,7 @@ interface UserModel {
updateUser: (userInput: UserEdit) => Promise<User>;
deleteUser: (id: string) => Promise<void>;
resetUserPassword: (id: string) => Promise<void>;
userLastAccessed: (userInput: User) => Promise<Boolean>;
transformKeycloakUsers: (keycloakUsers: UserRepresentation[]) => Promise<User[]>;
}

Expand Down Expand Up @@ -180,8 +182,14 @@ export const User = (clients: {
let usersWithGitlabIdFetch = [];

for (const user of users) {
// set the lastaccessed attribute
let date = null;
if (user['attributes']['last_accessed']) {
date = new Date(user['attributes']['last_accessed']*1000).toISOString()
}
usersWithGitlabIdFetch.push({
...user,
lastAccessed: date,
gitlabId: await fetchGitlabId(user)
});
}
Expand Down Expand Up @@ -534,6 +542,31 @@ export const User = (clients: {
}
};

const userLastAccessed = async (userInput: User): Promise<Boolean> => {
// set the last accessed as a unix timestamp on the user attributes
try {
const lastAccessed = {last_accessed: Math.floor(Date.now() / 1000)}
await keycloakAdminClient.users.update(
{
id: userInput.id
},
{
attributes: {
...userInput.attributes,
...lastAccessed
}
}
);
} catch (err) {
if (err.response.status && err.response.status === 404) {
throw new UserNotFoundError(`User not found: ${userInput.id}`);
} else {
logger.warn(`Error updating Keycloak user: ${err.message}`);
}
}
return true
};

const updateUser = async (userInput: UserEdit): Promise<User> => {
// comments used to be removed when updating a user, now they aren't
let organizations = null;
Expand Down Expand Up @@ -680,6 +713,7 @@ export const User = (clients: {
getUserRolesForProject,
addUser,
updateUser,
userLastAccessed,
deleteUser,
resetUserPassword,
transformKeycloakUsers
Expand Down
22 changes: 22 additions & 0 deletions services/api/src/routes/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,28 @@ const keysRoute = async (
logger.debug(`Unknown fingerprint: ${fingerprint}`);
}

// update key used timestamp
const foundkey = await query(
sqlClientPool,
knex('ssh_key')
.select('id')
.where('key_fingerprint', fingerprint)
.toString(),
);
// check if a key is found
if (foundkey.length > 0) {
var date = new Date();
const convertDateFormat = R.init;
var lastUsed = convertDateFormat(date.toISOString());
await query(
sqlClientPool,
knex('ssh_key')
.where('id', foundkey[0].id)
.update({lastUsed: lastUsed})
.toString(),
);
}

res.send(result);
};

Expand Down
2 changes: 2 additions & 0 deletions services/api/src/typeDefs.js
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,7 @@ const typeDefs = gql`
keyType: String
keyFingerprint: String
created: String
lastUsed: String
}
type User {
Expand All @@ -452,6 +453,7 @@ const typeDefs = gql`
# This just returns the group name, id and the role the user has in that group.
# This is a neat way to visualize a users specific access without having to get all members of a group
groupRoles: [GroupRoleInterface]
lastAccessed: String
}
type GroupMembership {
Expand Down
1 change: 1 addition & 0 deletions services/api/src/util/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ export const keycloakHasPermission = (grant, requestCache, modelClients, service
currentUser: [currentUser.id]
};

await UserModel.userLastAccessed(currentUser);

const usersAttribute = R.prop('users', attributes);
if (usersAttribute && usersAttribute.length) {
Expand Down

0 comments on commit ea51af4

Please sign in to comment.