Compare commits

...

4 Commits

Author SHA1 Message Date
beb54e8a12 Add scaffolding for email confirmation / account merging 2024-04-16 14:54:01 -07:00
d035403af1 Update kubernetes.mdx 2024-04-12 14:07:31 -04:00
1af0d958dd Update migration order for group 2024-04-12 10:50:06 -07:00
66a51658d7 Merge pull request #1682 from Infisical/k8s-owner-policy
add docs for owner policy
2024-04-12 12:52:09 -04:00
22 changed files with 324 additions and 28 deletions

View File

@ -21,7 +21,8 @@ export const UsersSchema = z.object({
createdAt: z.date(),
updatedAt: z.date(),
isGhost: z.boolean().default(false),
username: z.string()
username: z.string(),
isEmailVerified: z.boolean().default(false).nullable().optional()
});
export type TUsers = z.infer<typeof UsersSchema>;

View File

@ -29,6 +29,7 @@ export async function seed(knex: Knex): Promise<void> {
lastName: "",
authMethods: [AuthMethod.EMAIL],
isAccepted: true,
isEmailVerified: true,
isMfaEnabled: false,
mfaMethods: null,
devices: null

View File

@ -220,7 +220,8 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
})
)
.optional(),
// displayName: z.string().trim(),
externalId: z.string().trim(),
displayName: z.string().trim(),
active: z.boolean()
}),
response: {
@ -249,11 +250,12 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
const primaryEmail = req.body.emails?.find((email) => email.primary)?.value;
const user = await req.server.services.scim.createScimUser({
orgId: req.permission.orgId,
username: req.body.userName,
email: primaryEmail,
firstName: req.body.name.givenName,
lastName: req.body.name.familyName,
orgId: req.permission.orgId
externalId: req.body.externalId
});
return user;
@ -400,7 +402,12 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
schemas: z.array(z.string()),
id: z.string().trim(),
displayName: z.string().trim(),
members: z.array(z.any()).length(0)
members: z.array(
z.object({
value: z.string(),
display: z.string()
})
)
}),
response: {
200: z.object({
@ -482,8 +489,6 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => {
// console.log("PATCH /Groups/:groupId req.body: ", req.body);
// console.log("PATCH /Groups/:groupId req.body: ", req.body.Operations[0]);
const group = await req.server.services.scim.updateScimGroupNamePatch({
groupId: req.params.groupId,
orgId: req.permission.orgId,

View File

@ -24,10 +24,10 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
customAlerts: false,
auditLogs: false,
auditLogsRetentionDays: 0,
samlSSO: false,
scim: false,
samlSSO: true,
scim: true,
ldap: false,
groups: false,
groups: true,
status: null,
trial_end: null,
has_used_trial: true,

View File

@ -40,10 +40,10 @@ export type TFeatureSet = {
customAlerts: false;
auditLogs: false;
auditLogsRetentionDays: 0;
samlSSO: false;
scim: false;
samlSSO: true;
scim: true;
ldap: false;
groups: false;
groups: true;
status: null;
trial_end: null;
has_used_trial: true;

View File

@ -16,6 +16,7 @@ import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
@ -43,6 +44,7 @@ import {
type TScimServiceFactoryDep = {
scimDAL: Pick<TScimDALFactory, "create" | "find" | "findById" | "deleteById">;
userDAL: Pick<TUserDALFactory, "findOne" | "create" | "transaction">;
userAliasDAL: Pick<TUserAliasDALFactory, "create">;
orgDAL: Pick<
TOrgDALFactory,
"createMembership" | "findById" | "findMembership" | "deleteMembershipById" | "transaction"
@ -231,7 +233,7 @@ export const scimServiceFactory = ({
});
};
const createScimUser = async ({ username, email, firstName, lastName, orgId }: TCreateScimUserDTO) => {
const createScimUser = async ({ orgId, username, email, firstName, lastName }: TCreateScimUserDTO) => {
const org = await orgDAL.findById(orgId);
if (!org)
@ -473,7 +475,19 @@ export const scimServiceFactory = ({
};
const listScimGroups = async ({ orgId, offset, limit }: TListScimGroupsDTO) => {
const plan = await licenseService.getPlan(orgId);
if (!plan.groups)
throw new BadRequestError({
message: "Failed to list SCIM groups due to plan restriction. Upgrade plan to list SCIM groups."
});
const org = await orgDAL.findById(orgId);
if (!org) {
throw new ScimRequestError({
detail: "Organization Not Found",
status: 404
});
}
if (!org.scimEnabled)
throw new ScimRequestError({
@ -501,7 +515,19 @@ export const scimServiceFactory = ({
};
const createScimGroup = async ({ displayName, orgId }: TCreateScimGroupDTO) => {
const plan = await licenseService.getPlan(orgId);
if (!plan.groups)
throw new BadRequestError({
message: "Failed to create a SCIM group due to plan restriction. Upgrade plan to create a SCIM group."
});
const org = await orgDAL.findById(orgId);
if (!org) {
throw new ScimRequestError({
detail: "Organization Not Found",
status: 404
});
}
if (!org.scimEnabled)
throw new ScimRequestError({
@ -524,6 +550,12 @@ export const scimServiceFactory = ({
};
const getScimGroup = async ({ groupId, orgId }: TGetScimGroupDTO) => {
const plan = await licenseService.getPlan(orgId);
if (!plan.groups)
throw new BadRequestError({
message: "Failed to get SCIM group due to plan restriction. Upgrade plan to get SCIM group."
});
const group = await groupDAL.findOne({
id: groupId,
orgId
@ -554,6 +586,26 @@ export const scimServiceFactory = ({
};
const updateScimGroupNamePut = async ({ groupId, orgId, displayName }: TUpdateScimGroupNamePutDTO) => {
const plan = await licenseService.getPlan(orgId);
if (!plan.groups)
throw new BadRequestError({
message: "Failed to update SCIM group due to plan restriction. Upgrade plan to update SCIM group."
});
const org = await orgDAL.findById(orgId);
if (!org) {
throw new ScimRequestError({
detail: "Organization Not Found",
status: 404
});
}
if (!org.scimEnabled)
throw new ScimRequestError({
detail: "SCIM is disabled for the organization",
status: 403
});
const [group] = await groupDAL.update(
{
id: groupId,
@ -580,7 +632,19 @@ export const scimServiceFactory = ({
// TODO: add support for add/remove op
const updateScimGroupNamePatch = async ({ groupId, orgId, operations }: TUpdateScimGroupNamePatchDTO) => {
const plan = await licenseService.getPlan(orgId);
if (!plan.groups)
throw new BadRequestError({
message: "Failed to update SCIM group due to plan restriction. Upgrade plan to update SCIM group."
});
const org = await orgDAL.findById(orgId);
if (!org) {
throw new ScimRequestError({
detail: "Organization Not Found",
status: 404
});
}
if (!org.scimEnabled)
throw new ScimRequestError({
@ -635,6 +699,26 @@ export const scimServiceFactory = ({
};
const deleteScimGroup = async ({ groupId, orgId }: TDeleteScimGroupDTO) => {
const plan = await licenseService.getPlan(orgId);
if (!plan.groups)
throw new BadRequestError({
message: "Failed to delete SCIM group due to plan restriction. Upgrade plan to delete SCIM group."
});
const org = await orgDAL.findById(orgId);
if (!org) {
throw new ScimRequestError({
detail: "Organization Not Found",
status: 404
});
}
if (!org.scimEnabled)
throw new ScimRequestError({
detail: "SCIM is disabled for the organization",
status: 403
});
const [group] = await groupDAL.delete({
id: groupId,
orgId

View File

@ -37,6 +37,7 @@ export type TCreateScimUserDTO = {
firstName: string;
lastName: string;
orgId: string;
externalId: string;
};
export type TUpdateScimUserDTO = {

View File

@ -41,8 +41,8 @@ export const secretsLimit: RateLimitOptions = {
};
export const authRateLimit: RateLimitOptions = {
timeWindow: 60 * 1000,
max: 60,
timeWindow: 5 * 60 * 1000,
max: 30,
keyGenerator: (req) => req.realIp
};

View File

@ -286,6 +286,7 @@ export const registerRoutes = async (
licenseService,
scimDAL,
userDAL,
userAliasDAL,
orgDAL,
projectDAL,
projectMembershipDAL,
@ -315,7 +316,14 @@ export const registerRoutes = async (
});
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL });
const userService = userServiceFactory({ userDAL });
const userService = userServiceFactory({
userDAL,
orgDAL,
projectMembershipDAL,
projectUserMembershipRoleDAL,
tokenService,
smtpService
});
const loginService = authLoginServiceFactory({ userDAL, smtpService, tokenService, orgDAL, tokenDAL: authTokenDAL });
const passwordService = authPaswordServiceFactory({
tokenService,

View File

@ -2,11 +2,93 @@ import { z } from "zod";
import { AuthTokenSessionsSchema, OrganizationsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
import { ApiKeysSchema } from "@app/db/schemas/api-keys";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMethod, AuthMode } from "@app/services/auth/auth-type";
export const registerUserRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/me/emails/code",
config: {
rateLimit: authRateLimit
},
schema: {
response: {
200: z.object({})
}
},
preHandler: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
await server.services.user.sendEmailVerificationCode(req.permission.id);
return {};
}
});
server.route({
method: "POST",
url: "/me/emails/verify",
config: {
rateLimit: authRateLimit
},
schema: {
body: z.object({
code: z.string().trim()
}),
response: {
200: z.object({})
}
},
preHandler: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
await server.services.user.verifyEmailVerificationCode(req.permission.id, req.body.code);
return {};
}
});
server.route({
method: "GET",
url: "/me/users/same-email",
config: {
rateLimit: readLimit
},
schema: {
response: {
200: z.object({
users: UsersSchema.array()
})
}
},
preHandler: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const users = await server.services.user.listUsersWithSameEmail(req.permission.id);
return {
users
};
}
});
// server.route({ // TODO(dangtony98)
// method: "POST",
// url: "/me/users/merge",
// config: {
// rateLimit: authRateLimit
// },
// schema: {
// body: z.object({
// username: z.string().trim()
// }),
// response: {
// 200: z.object({})
// }
// },
// preHandler: verifyAuth([AuthMode.JWT]),
// handler: async (req) => {
// console.log("POST /me/users/merge req.body: ", req.body);
// return {};
// }
// });
server.route({
method: "PATCH",
url: "/me/mfa",

View File

@ -27,6 +27,11 @@ export const getTokenConfig = (tokenType: TokenType) => {
const expiresAt = new Date(new Date().getTime() + 86400000);
return { token, expiresAt };
}
case TokenType.TOKEN_EMAIL_VERIFICATION: {
const token = crypto.randomBytes(16).toString("hex");
const expiresAt = new Date(new Date().getTime() + 300000);
return { token, expiresAt };
}
case TokenType.TOKEN_EMAIL_MFA: {
// generate random 6-digit code
const token = String(crypto.randomInt(10 ** 5, 10 ** 6 - 1));

View File

@ -1,5 +1,6 @@
export enum TokenType {
TOKEN_EMAIL_CONFIRMATION = "emailConfirmation",
TOKEN_EMAIL_VERIFICATION = "emailVerification", // unverified -> verified email
TOKEN_EMAIL_MFA = "emailMfa",
TOKEN_EMAIL_ORG_INVITATION = "organizationInvitation",
TOKEN_EMAIL_PASSWORD_RESET = "passwordReset"

View File

@ -60,7 +60,7 @@ export const authSignupServiceFactory = ({
});
await smtpService.sendMail({
template: SmtpTemplates.EmailVerification,
template: SmtpTemplates.SignupEmailVerification,
subjectLine: "Infisical confirmation code",
recipients: [email],
substitutions: {
@ -129,7 +129,16 @@ export const authSignupServiceFactory = ({
}
const updateduser = await authDAL.transaction(async (tx) => {
const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx);
const us = await userDAL.updateById(
user.id,
{
firstName,
lastName,
isAccepted: true,
isEmailVerified: true
},
tx
);
if (!us) throw new Error("User not found");
const userEncKey = await userDAL.upsertUserEncryptionKey(
us.id,
@ -243,7 +252,16 @@ export const authSignupServiceFactory = ({
});
const updateduser = await authDAL.transaction(async (tx) => {
const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx);
const us = await userDAL.updateById(
user.id,
{
firstName,
lastName,
isAccepted: true,
isEmailVerified: true
},
tx
);
if (!us) throw new Error("User not found");
const userEncKey = await userDAL.upsertUserEncryptionKey(
us.id,

View File

@ -175,7 +175,8 @@ export const orgServiceFactory = ({
authMethods: [AuthMethod.EMAIL],
username: email,
email,
isAccepted: true
isAccepted: true,
isEmailVerified: false
},
tx
);

View File

@ -17,6 +17,7 @@ export type TSmtpSendMail = {
export type TSmtpService = ReturnType<typeof smtpServiceFactory>;
export enum SmtpTemplates {
SignupEmailVerification = "signupEmailVerification.handlebars",
EmailVerification = "emailVerification.handlebars",
SecretReminder = "secretReminder.handlebars",
EmailMfa = "emailMfa.handlebars",

View File

@ -9,9 +9,8 @@
<body>
<h2>Confirm your email address</h2>
<p>Your confirmation code is below — enter it in the browser window where you've started signing up for Infisical.</p>
<p>Your confirmation code is below — enter it in the browser window where you've started confirming your email.</p>
<h1>{{code}}</h1>
<p>Questions about setting up Infisical? Email us at support@infisical.com</p>
</body>
</html>

View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>Code</title>
</head>
<body>
<h2>Confirm your email address</h2>
<p>Your confirmation code is below — enter it in the browser window where you've started signing up for Infisical.</p>
<h1>{{code}}</h1>
<p>Questions about setting up Infisical? Email us at support@infisical.com</p>
</body>
</html>

View File

@ -102,7 +102,8 @@ export const superAdminServiceFactory = ({
superAdmin: true,
isGhost: false,
isAccepted: true,
authMethods: [AuthMethod.EMAIL]
authMethods: [AuthMethod.EMAIL],
isEmailVerified: false
},
tx
);

View File

@ -4,13 +4,17 @@ import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TUserDALFactory } from "@app/services/user/user-dal";
export const normalizeUsername = async (username: string, userDAL: Pick<TUserDALFactory, "findOne">) => {
let attempt = slugify(username);
let attempt = slugify(username, {
preserveCharacters: ["@", "."]
});
let user = await userDAL.findOne({ username: attempt });
if (!user) return attempt;
while (true) {
attempt = slugify(`${username}-${alphaNumericNanoId(4)}`);
attempt = slugify(`${username}-${alphaNumericNanoId(4)}`, {
preserveCharacters: ["@", "."]
});
// eslint-disable-next-line no-await-in-loop
user = await userDAL.findOne({ username: attempt });

View File

@ -1,15 +1,79 @@
import { BadRequestError } from "@app/lib/errors";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { TokenType } from "@app/services/auth-token/auth-token-types";
import { TOrgDALFactory } from "@app/services/org/org-dal";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { TProjectUserMembershipRoleDALFactory } from "@app/services/project-membership/project-user-membership-role-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { AuthMethod } from "../auth/auth-type";
import { TUserDALFactory } from "./user-dal";
type TUserServiceFactoryDep = {
userDAL: TUserDALFactory;
orgDAL: TOrgDALFactory;
projectMembershipDAL: TProjectMembershipDALFactory;
projectUserMembershipRoleDAL: TProjectUserMembershipRoleDALFactory;
tokenService: TAuthTokenServiceFactory;
smtpService: Pick<TSmtpService, "sendMail">;
};
export type TUserServiceFactory = ReturnType<typeof userServiceFactory>;
export const userServiceFactory = ({ userDAL }: TUserServiceFactoryDep) => {
export const userServiceFactory = ({ userDAL, tokenService, smtpService }: TUserServiceFactoryDep) => {
const sendEmailVerificationCode = async (userId: string) => {
const user = await userDAL.findById(userId);
if (!user) throw new BadRequestError({ name: "Failed to find user" });
if (!user.email)
throw new BadRequestError({ name: "Failed to send email verification code due to no email on user" });
if (user.isEmailVerified)
throw new BadRequestError({ name: "Failed to send email verification code due to email already verified" });
const token = await tokenService.createTokenForUser({
type: TokenType.TOKEN_EMAIL_VERIFICATION,
userId: user.id
});
await smtpService.sendMail({
template: SmtpTemplates.EmailVerification,
subjectLine: "Infisical confirmation code",
recipients: [user.email],
substitutions: {
code: token
}
});
};
const verifyEmailVerificationCode = async (userId: string, code: string) => {
const user = await userDAL.findById(userId);
if (!user) throw new BadRequestError({ name: "Failed to find user" });
if (user.isEmailVerified)
throw new BadRequestError({ name: "Failed to verify email verification code due to email already verified" });
await tokenService.validateTokenForUser({
type: TokenType.TOKEN_EMAIL_VERIFICATION,
userId: user.id,
code
});
await userDAL.updateById(userId, { isEmailVerified: true });
};
// lists users with same verified email only
const listUsersWithSameEmail = async (userId: string) => {
const user = await userDAL.findById(userId);
if (!user) throw new BadRequestError({ name: "Failed to find user" });
if (!user.email)
throw new BadRequestError({ name: "Failed to list users with same email due to no email on user" });
const users = await userDAL.find({
email: user.email,
isEmailVerified: true
});
return users;
};
const toggleUserMfa = async (userId: string, isMfaEnabled: boolean) => {
const user = await userDAL.findById(userId);
@ -72,6 +136,9 @@ export const userServiceFactory = ({ userDAL }: TUserServiceFactoryDep) => {
};
return {
sendEmailVerificationCode,
verifyEmailVerificationCode,
listUsersWithSameEmail,
toggleUserMfa,
updateUserName,
updateAuthMethods,

View File

@ -235,7 +235,7 @@ The namespace of the managed Kubernetes secret to be created.
Override the default Opaque type for managed secrets with this field. Useful for creating kubernetes.io/dockerconfigjson secrets.
</Accordion>
<Accordion title="managedSecretReference.creationPolicy">
Creation polices allow you to control whether or not to owner references should be added to the managed Kubernetes secret that is generated by the Infisical operator.
Creation polices allow you to control whether or not owner references should be added to the managed Kubernetes secret that is generated by the Infisical operator.
This is useful for tools such as ArgoCD, where every resource requires an owner reference; otherwise, it will be pruned automatically.
#### Available options