Skip to content

feat(v1.30.1): retrieve users' last login time and preview API keys #288

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

Merged
merged 7 commits into from
May 6, 2025
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
2 changes: 1 addition & 1 deletion .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ env:
WEAVIATE_127: 1.27.15
WEAVIATE_128: 1.28.11
WEAVIATE_129: 1.29.1
WEAVIATE_130: 1.30.0
WEAVIATE_130: 1.30.1

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
Expand Down
88 changes: 87 additions & 1 deletion src/openapi/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ export interface paths {
'/replication/replicate': {
post: operations['replicate'];
};
'/replication/replicate/{id}': {
/** Returns the details of a replication operation for a given shard, identified by the provided replication operation id. */
get: operations['replicationDetails'];
};
'/users/own-info': {
get: operations['getOwnInfo'];
};
Expand Down Expand Up @@ -286,6 +290,18 @@ export interface definitions {
dbUserType: 'db_user' | 'db_env_user';
/** @description activity status of the returned user */
active: boolean;
/**
* Format: date-time
* @description Date and time in ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ)
*/
createdAt?: unknown;
/** @description First 3 letters of the associated API-key */
apiKeyFirstLetters?: unknown;
/**
* Format: date-time
* @description Date and time in ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ)
*/
lastUsedAt?: unknown;
};
UserApiKey: {
/** @description The apikey */
Expand Down Expand Up @@ -568,6 +584,8 @@ export interface definitions {
indexNullState?: boolean;
/** @description Index length of properties (default: 'false'). */
indexPropertyLength?: boolean;
/** @description Using BlockMax WAND for query execution (default: 'false', will be 'true' for new collections created after 1.30). */
usingBlockMaxWAND?: boolean;
};
/** @description Configure how replication is executed in a cluster */
ReplicationConfig: {
Expand Down Expand Up @@ -689,6 +707,29 @@ export interface definitions {
/** @description The shard id holding the replica to be deleted */
shardId: string;
};
/** @description The current status and details of a replication operation, including information about the resources involved in the replication process. */
ReplicationReplicateDetailsReplicaResponse: {
/** @description The unique id of the replication operation. */
id: string;
/** @description The id of the shard to collect replication details for. */
shardId: string;
/** @description The name of the collection holding data being replicated. */
collection: string;
/** @description The id of the node where the source replica is allocated. */
sourceNodeId: string;
/** @description The id of the node where the target replica is allocated. */
targetNodeId: string;
/**
* @description The current status of the replication operation, indicating the replication phase the operation is in.
* @enum {string}
*/
status:
| 'READY'
| 'INDEXING'
| 'REPLICATION_FINALIZING'
| 'REPLICATION_HYDRATING'
| 'REPLICATION_DEHYDRATING';
};
/** @description A single peer in the network. */
PeerUpdate: {
/**
Expand Down Expand Up @@ -1728,6 +1769,37 @@ export interface operations {
};
};
};
/** Returns the details of a replication operation for a given shard, identified by the provided replication operation id. */
replicationDetails: {
parameters: {
path: {
/** The replication operation id to get details for. */
id: string;
};
};
responses: {
/** The details of the replication operation. */
200: {
schema: definitions['ReplicationReplicateDetailsReplicaResponse'];
};
/** Malformed request. */
400: {
schema: definitions['ErrorResponse'];
};
/** Unauthorized or invalid credentials. */
401: unknown;
/** Forbidden */
403: {
schema: definitions['ErrorResponse'];
};
/** Shard replica operation not found */
404: unknown;
/** An error has occurred while trying to fulfill the request. Most likely the ErrorResponse will contain more information about the error. */
500: {
schema: definitions['ErrorResponse'];
};
};
};
getOwnInfo: {
responses: {
/** Info about the user */
Expand All @@ -1743,8 +1815,14 @@ export interface operations {
};
};
listAllUsers: {
parameters: {
query: {
/** Whether to include the last used time of the users */
includeLastUsedTime?: boolean;
};
};
responses: {
/** Info about the user */
/** Info about the users */
200: {
schema: definitions['DBUserInfo'][];
};
Expand All @@ -1766,6 +1844,10 @@ export interface operations {
/** user id */
user_id: string;
};
query: {
/** Whether to include the last used time of the given user */
includeLastUsedTime?: boolean;
};
};
responses: {
/** Info about the user */
Expand All @@ -1780,6 +1862,10 @@ export interface operations {
};
/** user not found */
404: unknown;
/** Request body is well-formed (i.e., syntactically correct), but semantically erroneous. */
422: {
schema: definitions['ErrorResponse'];
};
/** An error has occurred while trying to fulfill the request. Most likely the ErrorResponse will contain more information about the error. */
500: {
schema: definitions['ErrorResponse'];
Expand Down
8 changes: 8 additions & 0 deletions src/roles/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ import {
UsersPermission,
} from './types.js';

/** ZERO_TIME is the timestamp Weaviate server sends in abscence of a value (null value). */
const ZERO_TIME = '0001-01-01T00:00:00.000Z';

export class PermissionGuards {
private static includes = <A extends string>(permission: Permission, ...actions: A[]): boolean =>
actions.filter((a) => Array.from<string>(permission.actions).includes(a)).length > 0;
Expand Down Expand Up @@ -155,13 +158,18 @@ export class Map {
id: user.userId,
roleNames: user.roles,
active: user.active,
createdAt: Map.unknownDate(user.createdAt),
lastUsedAt: Map.unknownDate(user.lastUsedAt),
apiKeyFirstLetters: user.apiKeyFirstLetters as string,
});
static dbUsers = (users: WeaviateDBUser[]): UserDB[] => users.map(Map.dbUser);
static assignedUsers = (users: WeaviateAssignedUser[]): UserAssignment[] =>
users.map((user) => ({
id: user.userId || '',
userType: user.userType,
}));
static unknownDate = (date?: unknown): Date | undefined =>
date !== undefined && typeof date === 'string' && date !== ZERO_TIME ? new Date(date) : undefined;
}

class PermissionsMapping {
Expand Down
30 changes: 22 additions & 8 deletions src/users/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@ import {
} from '../openapi/types.js';
import { Role } from '../roles/types.js';
import { Map } from '../roles/util.js';
import { AssignRevokeOptions, DeactivateOptions, GetAssignedRolesOptions, User, UserDB } from './types.js';
import {
AssignRevokeOptions,
DeactivateOptions,
GetAssignedRolesOptions,
GetUserOptions,
User,
UserDB,
} from './types.js';

/**
* Operations supported for 'db', 'oidc', and legacy (non-namespaced) users.
Expand Down Expand Up @@ -116,14 +123,14 @@ export interface DBUsers extends UsersBase {
* @param {string} userId The ID of the user to get.
* @returns {Promise<UserDB>} ID, status, and assigned roles of a 'db_*' user.
*/
byName: (userId: string) => Promise<UserDB>;
byName: (userId: string, opts?: GetUserOptions) => Promise<UserDB>;

/**
* List all 'db_user' / 'db_env_user' users.
*
* @returns {Promise<UserDB[]>} ID, status, and assigned roles for each 'db_*' user.
*/
listAll: () => Promise<UserDB[]>;
listAll: (opts?: GetUserOptions) => Promise<UserDB[]>;
}

/** Operations supported for namespaced 'oidc' users.*/
Expand Down Expand Up @@ -195,8 +202,17 @@ const db = (connection: ConnectionREST): DBUsers => {
.postEmpty<DeactivateOptions | null>(`/users/db/${userId}/deactivate`, opts || null)
.then(() => true)
.catch(expectCode(409)),
byName: (userId: string) => connection.get<WeaviateDBUser>(`/users/db/${userId}`, true).then(Map.dbUser),
listAll: () => connection.get<WeaviateDBUser[]>('/users/db', true).then(Map.dbUsers),
byName: (userId: string, opts?: GetUserOptions) =>
connection
.get<WeaviateDBUser>(
`/users/db/${userId}?includeLastUsedTime=${opts?.includeLastUsedTime || false}`,
true
)
.then(Map.dbUser),
listAll: (opts?: GetUserOptions) =>
connection
.get<WeaviateDBUser[]>(`/users/db?includeLastUsedTime=${opts?.includeLastUsedTime || false}`, true)
.then(Map.dbUsers),
};
};

Expand Down Expand Up @@ -238,9 +254,7 @@ const namespacedUsers = (connection: ConnectionREST): NamespacedUsers => {
getAssignedRoles: (userType: UserTypeInternal, userId: string, opts?: GetAssignedRolesOptions) =>
connection
.get<WeaviateRole[]>(
`/authz/users/${userId}/roles/${userType}${
opts?.includePermissions ? '?&includeFullRoles=true' : ''
}`
`/authz/users/${userId}/roles/${userType}?includeFullRoles=${opts?.includePermissions || false}`
)
.then(Map.roles),
assignRoles: (roleNames: string | string[], userId: string, opts?: AssignRevokeOptions) =>
Expand Down
Loading