feat: restrict non credential provider interactions (#871)
* wip: add provider field to sqlite user table * feat: disable invites when credentials provider is not used * wip: add migration for provider field in user table with sqlite * wip: remove fields that can not be modified by non credential users * wip: make username, mail and avatar disabled instead of hidden * wip: external users membership of group cannot be managed manually * feat: add alerts to inform about disabled fields and managing group members * wip: add mysql migration for provider on user table * chore: fix format issues * chore: address pull request feedback * fix: build issue * fix: deepsource issues * fix: tests not working * feat: restrict login to specific auth providers * chore: address pull request feedback * fix: deepsource issue
This commit is contained in:
9
packages/auth/providers/check-provider.ts
Normal file
9
packages/auth/providers/check-provider.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { SupportedAuthProvider } from "@homarr/definitions";
|
||||
|
||||
import { env } from "../env.mjs";
|
||||
|
||||
export const isProviderEnabled = (provider: SupportedAuthProvider) => {
|
||||
// The question mark is placed there because isProviderEnabled is called during static build of about page
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
return env.AUTH_PROVIDERS?.includes(provider);
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import bcrypt from "bcrypt";
|
||||
|
||||
import type { Database } from "@homarr/db";
|
||||
import { eq } from "@homarr/db";
|
||||
import { and, eq } from "@homarr/db";
|
||||
import { users } from "@homarr/db/schema/sqlite";
|
||||
import { logger } from "@homarr/log";
|
||||
import type { validation, z } from "@homarr/validation";
|
||||
@@ -11,7 +11,7 @@ export const authorizeWithBasicCredentialsAsync = async (
|
||||
credentials: z.infer<typeof validation.user.signIn>,
|
||||
) => {
|
||||
const user = await db.query.users.findFirst({
|
||||
where: eq(users.name, credentials.name),
|
||||
where: and(eq(users.name, credentials.name), eq(users.provider, "credentials")),
|
||||
});
|
||||
|
||||
if (!user?.password) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { Adapter } from "@auth/core/adapters";
|
||||
import { CredentialsSignin } from "@auth/core/errors";
|
||||
|
||||
import { createId } from "@homarr/db";
|
||||
import type { Database } from "@homarr/db";
|
||||
import { and, createId, eq } from "@homarr/db";
|
||||
import { users } from "@homarr/db/schema/sqlite";
|
||||
import { logger } from "@homarr/log";
|
||||
import type { validation } from "@homarr/validation";
|
||||
import { z } from "@homarr/validation";
|
||||
@@ -10,7 +11,7 @@ import { env } from "../../../env.mjs";
|
||||
import { LdapClient } from "../ldap-client";
|
||||
|
||||
export const authorizeWithLdapCredentialsAsync = async (
|
||||
adapter: Adapter,
|
||||
db: Database,
|
||||
credentials: z.infer<typeof validation.user.signIn>,
|
||||
) => {
|
||||
logger.info(`user ${credentials.name} is trying to log in using LDAP. Connecting to LDAP server...`);
|
||||
@@ -89,18 +90,30 @@ export const authorizeWithLdapCredentialsAsync = async (
|
||||
await client.disconnectAsync();
|
||||
|
||||
// Create or update user in the database
|
||||
let user = await adapter.getUserByEmail?.(mailResult.data);
|
||||
let user = await db.query.users.findFirst({
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
image: true,
|
||||
email: true,
|
||||
emailVerified: true,
|
||||
provider: true,
|
||||
},
|
||||
where: and(eq(users.email, mailResult.data), eq(users.provider, "ldap")),
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
logger.info(`User ${credentials.name} not found in the database. Creating...`);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
user = await adapter.createUser!({
|
||||
user = {
|
||||
id: createId(),
|
||||
name: credentials.name,
|
||||
email: mailResult.data,
|
||||
emailVerified: new Date(), // assume email is verified
|
||||
});
|
||||
image: null,
|
||||
provider: "ldap",
|
||||
};
|
||||
await db.insert(users).values(user);
|
||||
|
||||
logger.info(`User ${credentials.name} created successfully.`);
|
||||
}
|
||||
@@ -108,11 +121,9 @@ export const authorizeWithLdapCredentialsAsync = async (
|
||||
if (user.name !== credentials.name) {
|
||||
logger.warn(`User ${credentials.name} found in the database but with different name. Updating...`);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
user = await adapter.updateUser!({
|
||||
id: user.id,
|
||||
name: credentials.name,
|
||||
});
|
||||
user.name = credentials.name;
|
||||
|
||||
await db.update(users).set({ name: user.name }).where(eq(users.id, user.id));
|
||||
|
||||
logger.info(`User ${credentials.name} updated successfully.`);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import type Credentials from "@auth/core/providers/credentials";
|
||||
import type { Database } from "@homarr/db";
|
||||
import { validation } from "@homarr/validation";
|
||||
|
||||
import { adapter } from "../../adapter";
|
||||
import { authorizeWithBasicCredentialsAsync } from "./authorization/basic-authorization";
|
||||
import { authorizeWithLdapCredentialsAsync } from "./authorization/ldap-authorization";
|
||||
|
||||
@@ -32,7 +31,7 @@ export const createCredentialsConfiguration = (db: Database) =>
|
||||
const data = await validation.user.signIn.parseAsync(credentials);
|
||||
|
||||
if (data.credentialType === "ldap") {
|
||||
return await authorizeWithLdapCredentialsAsync(adapter, data).catch(() => null);
|
||||
return await authorizeWithLdapCredentialsAsync(db, data).catch(() => null);
|
||||
}
|
||||
|
||||
return await authorizeWithBasicCredentialsAsync(db, data);
|
||||
|
||||
@@ -32,6 +32,7 @@ export const OidcProvider = (headers: ReadonlyHeaders | null): OIDCConfig<Profil
|
||||
// Use the name as the username if the preferred_username is an email address
|
||||
name: profile.preferred_username.includes("@") ? profile.name : profile.preferred_username,
|
||||
email: profile.email,
|
||||
provider: "oidc",
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { Adapter } from "@auth/core/adapters";
|
||||
import { CredentialsSignin } from "@auth/core/errors";
|
||||
import { DrizzleAdapter } from "@auth/drizzle-adapter";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
import { createId, eq } from "@homarr/db";
|
||||
import type { Database } from "@homarr/db";
|
||||
import { and, createId, eq } from "@homarr/db";
|
||||
import { users } from "@homarr/db/schema/sqlite";
|
||||
import { createDb } from "@homarr/db/test";
|
||||
|
||||
@@ -32,7 +31,7 @@ describe("authorizeWithLdapCredentials", () => {
|
||||
|
||||
// Act
|
||||
const act = () =>
|
||||
authorizeWithLdapCredentialsAsync(null as unknown as Adapter, {
|
||||
authorizeWithLdapCredentialsAsync(null as unknown as Database, {
|
||||
name: "test",
|
||||
password: "test",
|
||||
credentialType: "ldap",
|
||||
@@ -55,7 +54,7 @@ describe("authorizeWithLdapCredentials", () => {
|
||||
|
||||
// Act
|
||||
const act = () =>
|
||||
authorizeWithLdapCredentialsAsync(null as unknown as Adapter, {
|
||||
authorizeWithLdapCredentialsAsync(null as unknown as Database, {
|
||||
name: "test",
|
||||
password: "test",
|
||||
credentialType: "ldap",
|
||||
@@ -85,7 +84,7 @@ describe("authorizeWithLdapCredentials", () => {
|
||||
|
||||
// Act
|
||||
const act = () =>
|
||||
authorizeWithLdapCredentialsAsync(null as unknown as Adapter, {
|
||||
authorizeWithLdapCredentialsAsync(null as unknown as Database, {
|
||||
name: "test",
|
||||
password: "test",
|
||||
credentialType: "ldap",
|
||||
@@ -118,7 +117,7 @@ describe("authorizeWithLdapCredentials", () => {
|
||||
|
||||
// Act
|
||||
const act = () =>
|
||||
authorizeWithLdapCredentialsAsync(null as unknown as Adapter, {
|
||||
authorizeWithLdapCredentialsAsync(null as unknown as Database, {
|
||||
name: "test",
|
||||
password: "test",
|
||||
credentialType: "ldap",
|
||||
@@ -132,7 +131,6 @@ describe("authorizeWithLdapCredentials", () => {
|
||||
test("should authorize user with correct credentials and create user", async () => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
const adapter = DrizzleAdapter(db);
|
||||
const spy = vi.spyOn(ldapClient, "LdapClient");
|
||||
spy.mockImplementation(
|
||||
() =>
|
||||
@@ -151,7 +149,7 @@ describe("authorizeWithLdapCredentials", () => {
|
||||
);
|
||||
|
||||
// Act
|
||||
const result = await authorizeWithLdapCredentialsAsync(adapter, {
|
||||
const result = await authorizeWithLdapCredentialsAsync(db, {
|
||||
name: "test",
|
||||
password: "test",
|
||||
credentialType: "ldap",
|
||||
@@ -166,13 +164,68 @@ describe("authorizeWithLdapCredentials", () => {
|
||||
expect(dbUser?.id).toBe(result.id);
|
||||
expect(dbUser?.email).toBe("test@gmail.com");
|
||||
expect(dbUser?.emailVerified).not.toBeNull();
|
||||
expect(dbUser?.provider).toBe("ldap");
|
||||
});
|
||||
|
||||
test("should authorize user with correct credentials and create user with same email when credentials user already exists", async () => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
const spy = vi.spyOn(ldapClient, "LdapClient");
|
||||
const salt = await createSaltAsync();
|
||||
spy.mockImplementation(
|
||||
() =>
|
||||
({
|
||||
bindAsync: vi.fn(() => Promise.resolve()),
|
||||
searchAsync: vi.fn(() =>
|
||||
Promise.resolve([
|
||||
{
|
||||
dn: "test",
|
||||
mail: "test@gmail.com",
|
||||
},
|
||||
]),
|
||||
),
|
||||
disconnectAsync: vi.fn(),
|
||||
}) as unknown as ldapClient.LdapClient,
|
||||
);
|
||||
await db.insert(users).values({
|
||||
id: createId(),
|
||||
name: "test",
|
||||
salt,
|
||||
password: await hashPasswordAsync("test", salt),
|
||||
email: "test@gmail.com",
|
||||
provider: "credentials",
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await authorizeWithLdapCredentialsAsync(db, {
|
||||
name: "test",
|
||||
password: "test",
|
||||
credentialType: "ldap",
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(result.name).toBe("test");
|
||||
const dbUser = await db.query.users.findFirst({
|
||||
where: and(eq(users.name, "test"), eq(users.provider, "ldap")),
|
||||
});
|
||||
expect(dbUser).toBeDefined();
|
||||
expect(dbUser?.id).toBe(result.id);
|
||||
expect(dbUser?.email).toBe("test@gmail.com");
|
||||
expect(dbUser?.emailVerified).not.toBeNull();
|
||||
expect(dbUser?.provider).toBe("ldap");
|
||||
|
||||
const credentialsUser = await db.query.users.findFirst({
|
||||
where: and(eq(users.name, "test"), eq(users.provider, "credentials")),
|
||||
});
|
||||
|
||||
expect(credentialsUser).toBeDefined();
|
||||
expect(credentialsUser?.id).not.toBe(result.id);
|
||||
});
|
||||
|
||||
test("should authorize user with correct credentials and update name", async () => {
|
||||
// Arrange
|
||||
const userId = createId();
|
||||
const db = createDb();
|
||||
const adapter = DrizzleAdapter(db);
|
||||
const salt = await createSaltAsync();
|
||||
await db.insert(users).values({
|
||||
id: userId,
|
||||
@@ -180,10 +233,11 @@ describe("authorizeWithLdapCredentials", () => {
|
||||
salt,
|
||||
password: await hashPasswordAsync("test", salt),
|
||||
email: "test@gmail.com",
|
||||
provider: "ldap",
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await authorizeWithLdapCredentialsAsync(adapter, {
|
||||
const result = await authorizeWithLdapCredentialsAsync(db, {
|
||||
name: "test",
|
||||
password: "test",
|
||||
credentialType: "ldap",
|
||||
@@ -200,5 +254,6 @@ describe("authorizeWithLdapCredentials", () => {
|
||||
expect(dbUser?.id).toBe(userId);
|
||||
expect(dbUser?.name).toBe("test");
|
||||
expect(dbUser?.email).toBe("test@gmail.com");
|
||||
expect(dbUser?.provider).toBe("ldap");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { hasQueryAccessToIntegrationsAsync } from "./permissions/integration-query-permissions";
|
||||
export { isProviderEnabled } from "./providers/check-provider";
|
||||
|
||||
Reference in New Issue
Block a user