From 2fb4b261a8a6d5f3feed66a8f4070b1700782b25 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sat, 11 Feb 2023 11:08:35 -0800 Subject: [PATCH 1/9] Turn off auto delete and manual check ttl for token --- backend/src/config/index.ts | 4 ++-- .../src/controllers/v1/membershipOrgController.ts | 11 ++++++----- backend/src/controllers/v1/passwordController.ts | 5 +++-- backend/src/helpers/signup.ts | 15 +++++++++++++-- backend/src/models/token.ts | 10 +++++----- 5 files changed, 29 insertions(+), 16 deletions(-) diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index ad1acb1a6..a7194308e 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -1,5 +1,5 @@ const PORT = process.env.PORT || 4000; -const EMAIL_TOKEN_LIFETIME = process.env.EMAIL_TOKEN_LIFETIME! || '86400'; +const EMAIL_TOKEN_LIFETIME = parseInt(process.env.EMAIL_TOKEN_LIFETIME! || '86400'); const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY!; const SALT_ROUNDS = parseInt(process.env.SALT_ROUNDS!) || 10; const JWT_AUTH_LIFETIME = process.env.JWT_AUTH_LIFETIME! || '10d'; @@ -24,7 +24,7 @@ const CLIENT_SECRET_HEROKU = process.env.CLIENT_SECRET_HEROKU!; const CLIENT_SECRET_VERCEL = process.env.CLIENT_SECRET_VERCEL!; const CLIENT_SECRET_NETLIFY = process.env.CLIENT_SECRET_NETLIFY!; const CLIENT_SECRET_GITHUB = process.env.CLIENT_SECRET_GITHUB!; -const CLIENT_SLUG_VERCEL= process.env.CLIENT_SLUG_VERCEL!; +const CLIENT_SLUG_VERCEL = process.env.CLIENT_SLUG_VERCEL!; const POSTHOG_HOST = process.env.POSTHOG_HOST! || 'https://app.posthog.com'; const POSTHOG_PROJECT_API_KEY = process.env.POSTHOG_PROJECT_API_KEY! || diff --git a/backend/src/controllers/v1/membershipOrgController.ts b/backend/src/controllers/v1/membershipOrgController.ts index 0040b583e..324be6c2e 100644 --- a/backend/src/controllers/v1/membershipOrgController.ts +++ b/backend/src/controllers/v1/membershipOrgController.ts @@ -1,7 +1,7 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; import crypto from 'crypto'; -import { SITE_URL, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../../config'; +import { SITE_URL, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, EMAIL_TOKEN_LIFETIME } from '../../config'; import { MembershipOrg, Organization, User, Token } from '../../models'; import { deleteMembershipOrg as deleteMemberFromOrg } from '../../helpers/membershipOrg'; import { checkEmailVerification } from '../../helpers/signup'; @@ -113,14 +113,14 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => { if (!membershipOrg) { throw new Error('Failed to validate organization membership'); } - + invitee = await User.findOne({ email: inviteeEmail }).select('+publicKey'); if (invitee) { // case: invitee is an existing user - + inviteeMembershipOrg = await MembershipOrg.findOne({ user: invitee._id, organization: organizationId @@ -170,7 +170,8 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => { { email: inviteeEmail, token, - createdAt: new Date() + createdAt: new Date(), + ttl: Math.floor(+new Date() / 1000) + EMAIL_TOKEN_LIFETIME // time in seconds, i.e unix }, { upsert: true, new: true } ); @@ -241,7 +242,7 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => { message: 'Successfully verified email', user, }); - } + } if (!user) { // initialize user account diff --git a/backend/src/controllers/v1/passwordController.ts b/backend/src/controllers/v1/passwordController.ts index 0c5530f58..25529cd10 100644 --- a/backend/src/controllers/v1/passwordController.ts +++ b/backend/src/controllers/v1/passwordController.ts @@ -8,7 +8,7 @@ import { User, Token, BackupPrivateKey, LoginSRPDetail } from '../../models'; import { checkEmailVerification } from '../../helpers/signup'; import { createToken } from '../../helpers/auth'; import { sendMail } from '../../helpers/nodemailer'; -import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, SITE_URL } from '../../config'; +import { EMAIL_TOKEN_LIFETIME, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, SITE_URL } from '../../config'; import { BadRequestError } from '../../utils/errors'; /** @@ -39,7 +39,8 @@ export const emailPasswordReset = async (req: Request, res: Response) => { { email, token, - createdAt: new Date() + createdAt: new Date(), + ttl: Math.floor(+new Date() / 1000) + EMAIL_TOKEN_LIFETIME // time in seconds, i.e unix }, { upsert: true, new: true } ); diff --git a/backend/src/helpers/signup.ts b/backend/src/helpers/signup.ts index 1ef1219f8..95b67ba6c 100644 --- a/backend/src/helpers/signup.ts +++ b/backend/src/helpers/signup.ts @@ -7,6 +7,7 @@ import { createWorkspace } from './workspace'; import { addMemberships } from './membership'; import { OWNER, ADMIN, ACCEPTED } from '../variables'; import { sendMail } from '../helpers/nodemailer'; +import { EMAIL_TOKEN_LIFETIME } from '../config'; /** * Send magic link to verify email to [email] @@ -25,7 +26,8 @@ const sendEmailVerification = async ({ email }: { email: string }) => { { email, token, - createdAt: new Date() + createdAt: new Date(), + ttl: Math.floor(+new Date() / 1000) + EMAIL_TOKEN_LIFETIME // time in seconds, i.e unix }, { upsert: true, new: true } ); @@ -62,11 +64,20 @@ const checkEmailVerification = async ({ code: string; }) => { try { - const token = await Token.findOneAndDelete({ + const token = await Token.findOne({ email, token: code }); + if (token && Math.floor(Date.now() / 1000) > token.ttl) { + await Token.deleteOne({ + email, + token: code + }); + + throw new Error('Verification token has expired') + } + if (!token) throw new Error('Failed to find email verification token'); } catch (err) { Sentry.setUser(null); diff --git a/backend/src/models/token.ts b/backend/src/models/token.ts index 9569aee0b..2da62d813 100644 --- a/backend/src/models/token.ts +++ b/backend/src/models/token.ts @@ -5,6 +5,7 @@ export interface IToken { email: string; token: string; createdAt: Date; + ttl: Number; } const tokenSchema = new Schema({ @@ -19,14 +20,13 @@ const tokenSchema = new Schema({ createdAt: { type: Date, default: Date.now + }, + ttl: { + type: Number, } }); -tokenSchema.index({ - createdAt: 1 -}, { - expireAfterSeconds: parseInt(EMAIL_TOKEN_LIFETIME) -}); +tokenSchema.index({ email: 1 }); const Token = model('Token', tokenSchema); From 2f1a671121ccedd74d1303ae8708a3960c5102b4 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sat, 11 Feb 2023 15:16:33 -0800 Subject: [PATCH 2/9] add workspace-memberships api --- .../controllers/v1/organizationController.ts | 43 ++++++++++++++++++- backend/src/routes/v1/organization.ts | 15 +++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/backend/src/controllers/v1/organizationController.ts b/backend/src/controllers/v1/organizationController.ts index 6fbccffeb..eaf58fad7 100644 --- a/backend/src/controllers/v1/organizationController.ts +++ b/backend/src/controllers/v1/organizationController.ts @@ -14,11 +14,13 @@ import { MembershipOrg, Organization, Workspace, - IncidentContactOrg + IncidentContactOrg, + IMembershipOrg } from '../../models'; import { createOrganization as create } from '../../helpers/organization'; import { addMembershipsOrg } from '../../helpers/membershipOrg'; import { OWNER, ACCEPTED } from '../../variables'; +import _ from 'lodash'; export const getOrganizations = async (req: Request, res: Response) => { let organizations; @@ -382,3 +384,42 @@ export const getOrganizationSubscriptions = async ( subscriptions }); }; + + +/** + * Given a org id, return the projects each member of the org belongs to + * @param req + * @param res + * @returns + */ +export const getOrganizationMembersAndTheirWorkspaces = async ( + req: Request, + res: Response +) => { + const { organizationId } = req.params; + const orgMemberships = await MembershipOrg.find({ organization: organizationId }); + const userIds = orgMemberships.map(orgMembership => orgMembership.user); + const memberships = await Membership.find({ user: { $in: userIds } }); + const userToWorkspaceIds: any = {}; + + memberships.forEach(membership => { + const user = membership.user.toString(); + if (userToWorkspaceIds[user]) { + userToWorkspaceIds[user].push(membership.workspace); + } else { + userToWorkspaceIds[user] = [membership.workspace]; + } + }); + + const workspaceIds = Object.values(userToWorkspaceIds).flat() + const workspacesList = await Workspace.find({ + organization: organizationId, + _id: { $in: workspaceIds } + }); + + const populatedUserWorkspaces = _.mapValues(userToWorkspaceIds, workspaceIds => + _.map(workspaceIds, id => _.find(workspacesList, { _id: id })) + ); + + return res.json(populatedUserWorkspaces); +}; \ No newline at end of file diff --git a/backend/src/routes/v1/organization.ts b/backend/src/routes/v1/organization.ts index 65a4b373f..314f684ad 100644 --- a/backend/src/routes/v1/organization.ts +++ b/backend/src/routes/v1/organization.ts @@ -156,4 +156,19 @@ router.get( organizationController.getOrganizationSubscriptions ); +router.get( + '/:organizationId/workspace-memberships', + requireAuth({ + acceptedAuthModes: ['jwt'] + }), + requireOrganizationAuth({ + acceptedRoles: [OWNER, ADMIN, MEMBER], + acceptedStatuses: [ACCEPTED] + }), + param('organizationId').exists().trim(), + validateRequest, + organizationController.getOrganizationMembersAndTheirWorkspaces +); + + export default router; From b066a55ead05887a98493451fb78223f50558329 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sat, 11 Feb 2023 23:41:51 -0800 Subject: [PATCH 3/9] Show only secret keys if write only access --- .../src/controllers/v2/secretsController.ts | 104 +++++++++++++++--- .../ee/helpers/checkMembershipPermissions.ts | 36 ++++++ 2 files changed, 122 insertions(+), 18 deletions(-) diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index 7267ae50c..29ef67000 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -17,7 +17,7 @@ import { EESecretService, EELogService } from '../../ee/services'; import { postHogClient } from '../../services'; import { getChannelFromUserAgent } from '../../utils/posthog'; import { ABILITY_READ, ABILITY_WRITE } from '../../variables/organization'; -import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissions'; +import { userHasNoAbility, userHasWorkspaceAccess, userHasWriteOnlyAbility } from '../../ee/helpers/checkMembershipPermissions'; /** * Create secret(s) for workspace with id [workspaceId] and environment [environment] @@ -298,27 +298,42 @@ export const getSecrets = async (req: Request, res: Response) => { userEmail = req.serviceTokenData.user.email; } - // none service token case as service tokens are already scoped + // none service token case as service tokens are already scoped to env and project + let hasWriteOnlyAccess if (!req.serviceTokenData) { - const hasAccess = await userHasWorkspaceAccess(userId, workspaceId, environment, ABILITY_READ) - if (!hasAccess) { + hasWriteOnlyAccess = await userHasWriteOnlyAbility(userId, workspaceId, environment) + const hasNoAccess = await userHasNoAbility(userId, workspaceId, environment) + if (hasNoAccess) { throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" }) } } - - const [err, secrets] = await to(Secret.find( - { - workspace: workspaceId, - environment, - $or: [ - { user: userId }, - { user: { $exists: false } } - ], - type: { $in: [SECRET_SHARED, SECRET_PERSONAL] } - } - ).populate("tags").then()) - - if (err) throw ValidationError({ message: 'Failed to get secrets', stack: err.stack }); + let secrets: any + if (hasWriteOnlyAccess) { + secrets = await Secret.find( + { + workspace: workspaceId, + environment, + $or: [ + { user: userId }, + { user: { $exists: false } } + ], + type: { $in: [SECRET_SHARED, SECRET_PERSONAL] } + } + ) + .select("secretKeyCiphertext secretKeyIV secretKeyTag") + } else { + secrets = await Secret.find( + { + workspace: workspaceId, + environment, + $or: [ + { user: userId }, + { user: { $exists: false } } + ], + type: { $in: [SECRET_SHARED, SECRET_PERSONAL] } + } + ).populate("tags") + } const channel = getChannelFromUserAgent(req.headers['user-agent']) @@ -356,6 +371,59 @@ export const getSecrets = async (req: Request, res: Response) => { }); } + +export const getOnlySecretKeys = async (req: Request, res: Response) => { + const { workspaceId, environment } = req.query; + + let userId = "" // used for getting personal secrets for user + let userEmail = "" // used for posthog + if (req.user) { + userId = req.user._id; + userEmail = req.user.email; + } + + if (req.serviceTokenData) { + userId = req.serviceTokenData.user._id + userEmail = req.serviceTokenData.user.email; + } + + // none service token case as service tokens are already scoped + if (!req.serviceTokenData) { + const hasAccess = await userHasWorkspaceAccess(userId, workspaceId, environment, ABILITY_READ) + if (!hasAccess) { + throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" }) + } + } + + const [err, secretKeys] = await to(Secret.find( + { + workspace: workspaceId, + environment, + $or: [ + { user: userId }, + { user: { $exists: false } } + ], + type: { $in: [SECRET_SHARED, SECRET_PERSONAL] } + } + ) + .select("secretKeyIV secretKeyTag secretKeyCiphertext") + .then()) + + if (err) throw ValidationError({ message: 'Failed to get secrets', stack: err.stack }); + + // readAction && await EELogService.createLog({ + // userId: new Types.ObjectId(userId), + // workspaceId: new Types.ObjectId(workspaceId as string), + // actions: [readAction], + // channel, + // ipAddress: req.ip + // }); + + return res.status(200).send({ + secretKeys + }); +} + /** * Update secret(s) * @param req diff --git a/backend/src/ee/helpers/checkMembershipPermissions.ts b/backend/src/ee/helpers/checkMembershipPermissions.ts index 55155e885..50cd28917 100644 --- a/backend/src/ee/helpers/checkMembershipPermissions.ts +++ b/backend/src/ee/helpers/checkMembershipPermissions.ts @@ -1,5 +1,6 @@ import _ from "lodash"; import { Membership } from "../../models"; +import { ABILITY_READ, ABILITY_WRITE } from "../../variables/organization"; export const userHasWorkspaceAccess = async (userId: any, workspaceId: any, environment: any, action: any) => { const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId }) @@ -15,4 +16,39 @@ export const userHasWorkspaceAccess = async (userId: any, workspaceId: any, envi } return true +} + +export const userHasWriteOnlyAbility = async (userId: any, workspaceId: any, environment: any) => { + const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId }) + if (!membershipForWorkspace) { + return false + } + + const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions; + const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_WRITE }); + const isReadDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_READ }); + + // case: you have write only if read is blocked and write is not + if (isReadDisallowed && !isWriteDisallowed) { + return true + } + + return false +} + +export const userHasNoAbility = async (userId: any, workspaceId: any, environment: any) => { + const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId }) + if (!membershipForWorkspace) { + return true + } + + const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions; + const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_WRITE }); + const isReadBlocked = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_READ }); + + if (isReadBlocked && isWriteDisallowed) { + return true + } + + return false } \ No newline at end of file From 409de81bd2dad59699fb4bd9f40af5a9d6065997 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sun, 12 Feb 2023 09:34:52 -0800 Subject: [PATCH 4/9] Allow sign up disable --- backend/src/config/index.ts | 2 ++ backend/src/controllers/v1/signupController.ts | 9 +++++++-- docs/self-hosting/configuration/envars.mdx | 3 ++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index a7194308e..1ea6bc3a1 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -1,5 +1,6 @@ const PORT = process.env.PORT || 4000; const EMAIL_TOKEN_LIFETIME = parseInt(process.env.EMAIL_TOKEN_LIFETIME! || '86400'); +const DISABLE_NEW_SIGN_UP = process.env.DISABLE_NEW_SIGN_UP == undefined ? false : process.env.DISABLE_NEW_SIGN_UP const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY!; const SALT_ROUNDS = parseInt(process.env.SALT_ROUNDS!) || 10; const JWT_AUTH_LIFETIME = process.env.JWT_AUTH_LIFETIME! || '10d'; @@ -50,6 +51,7 @@ const LICENSE_KEY = process.env.LICENSE_KEY!; export { PORT, EMAIL_TOKEN_LIFETIME, + DISABLE_NEW_SIGN_UP, ENCRYPTION_KEY, SALT_ROUNDS, JWT_AUTH_LIFETIME, diff --git a/backend/src/controllers/v1/signupController.ts b/backend/src/controllers/v1/signupController.ts index 62e5a62a3..dc9860f43 100644 --- a/backend/src/controllers/v1/signupController.ts +++ b/backend/src/controllers/v1/signupController.ts @@ -1,6 +1,6 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; -import { NODE_ENV, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../../config'; +import { NODE_ENV, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, DISABLE_NEW_SIGN_UP } from '../../config'; import { User, MembershipOrg } from '../../models'; import { completeAccount } from '../../helpers/user'; import { @@ -11,6 +11,7 @@ import { import { issueTokens, createToken } from '../../helpers/auth'; import { INVITED, ACCEPTED } from '../../variables'; import axios from 'axios'; +import { BadRequestError } from '../../utils/errors'; /** * Signup step 1: Initialize account for user under email [email] and send a verification code @@ -24,6 +25,10 @@ export const beginEmailSignup = async (req: Request, res: Response) => { try { email = req.body.email; + if (DISABLE_NEW_SIGN_UP) { + throw BadRequestError({ message: "New signups are not permitted at this time" }) + } + const user = await User.findOne({ email }).select('+publicKey'); if (user && user?.publicKey) { // case: user has already completed account @@ -129,7 +134,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => { // get user user = await User.findOne({ email }); - + if (!user || (user && user?.publicKey)) { // case 1: user doesn't exist. // case 2: user has already completed account diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index 39d6542c9..42af4da79 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -37,5 +37,6 @@ Configuring Infisical requires setting some environment variables. There is a fi | `CLIENT_SECRET_VERCEL` | OAuth2 client secret for Vercel integration | `None` | | `CLIENT_SECRET_NETLIFY` | OAuth2 client secret for Netlify integration | `None` | | `CLIENT_SECRET_GITHUB` | OAuth2 client secret for GitHub integration | `None` | -| `CLIENT_SLUG_VERCEL` | OAuth2 slug for Netlify integration | `None` | +| `CLIENT_SLUG_VERCEL` | OAuth2 slug for Netlify integration | `None` | | `SENTRY_DSN` | DSN for error-monitoring with Sentry | `None` | +| `DISABLE_NEW_SIGN_UP` | Block new sign ups on your self hosted instance | `false` | From 2022988e773a432dcbb25dbb4f7622dae13448e9 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sun, 12 Feb 2023 10:34:32 -0800 Subject: [PATCH 5/9] Only allow sign up when invted --- backend/src/config/index.ts | 4 ++-- backend/src/controllers/v1/signupController.ts | 10 +++++++--- docs/self-hosting/configuration/envars.mdx | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 1ea6bc3a1..1d8665c72 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -1,6 +1,6 @@ const PORT = process.env.PORT || 4000; const EMAIL_TOKEN_LIFETIME = parseInt(process.env.EMAIL_TOKEN_LIFETIME! || '86400'); -const DISABLE_NEW_SIGN_UP = process.env.DISABLE_NEW_SIGN_UP == undefined ? false : process.env.DISABLE_NEW_SIGN_UP +const INVITE_ONLY_SIGNUP = process.env.INVITE_ONLY_SIGNUP == undefined ? false : process.env.INVITE_ONLY_SIGNUP const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY!; const SALT_ROUNDS = parseInt(process.env.SALT_ROUNDS!) || 10; const JWT_AUTH_LIFETIME = process.env.JWT_AUTH_LIFETIME! || '10d'; @@ -51,7 +51,7 @@ const LICENSE_KEY = process.env.LICENSE_KEY!; export { PORT, EMAIL_TOKEN_LIFETIME, - DISABLE_NEW_SIGN_UP, + INVITE_ONLY_SIGNUP, ENCRYPTION_KEY, SALT_ROUNDS, JWT_AUTH_LIFETIME, diff --git a/backend/src/controllers/v1/signupController.ts b/backend/src/controllers/v1/signupController.ts index dc9860f43..dad9632db 100644 --- a/backend/src/controllers/v1/signupController.ts +++ b/backend/src/controllers/v1/signupController.ts @@ -1,6 +1,6 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; -import { NODE_ENV, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, DISABLE_NEW_SIGN_UP } from '../../config'; +import { NODE_ENV, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, INVITE_ONLY_SIGNUP } from '../../config'; import { User, MembershipOrg } from '../../models'; import { completeAccount } from '../../helpers/user'; import { @@ -25,8 +25,12 @@ export const beginEmailSignup = async (req: Request, res: Response) => { try { email = req.body.email; - if (DISABLE_NEW_SIGN_UP) { - throw BadRequestError({ message: "New signups are not permitted at this time" }) + if (INVITE_ONLY_SIGNUP) { + // Only one user can create an account without being invited. The rest need to be invited in order to make an account + const userCount = await User.countDocuments({}) + if (userCount != 0) { + throw BadRequestError({ message: "New user sign ups are not allowed at this time. You must be invited to sign up." }) + } } const user = await User.findOne({ email }).select('+publicKey'); diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index 42af4da79..804df78c2 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -39,4 +39,4 @@ Configuring Infisical requires setting some environment variables. There is a fi | `CLIENT_SECRET_GITHUB` | OAuth2 client secret for GitHub integration | `None` | | `CLIENT_SLUG_VERCEL` | OAuth2 slug for Netlify integration | `None` | | `SENTRY_DSN` | DSN for error-monitoring with Sentry | `None` | -| `DISABLE_NEW_SIGN_UP` | Block new sign ups on your self hosted instance | `false` | +| `INVITE_ONLY_SIGNUP` | If true, users can only sign up if they are invited | `false` | From a61233d2ba0241352e5789a97bdd49b91c679791 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sun, 12 Feb 2023 14:22:59 -0800 Subject: [PATCH 6/9] Release docker images for cli --- .github/workflows/release_build.yml | 12 ++-- .goreleaser.yaml | 88 +++++++++++++---------------- cli/docker/Dockerfile | 4 ++ 3 files changed, 52 insertions(+), 52 deletions(-) create mode 100644 cli/docker/Dockerfile diff --git a/.github/workflows/release_build.yml b/.github/workflows/release_build.yml index 3d11a1157..af395ce6c 100644 --- a/.github/workflows/release_build.yml +++ b/.github/workflows/release_build.yml @@ -4,7 +4,7 @@ on: push: # run only against tags tags: - - 'v*' + - "v*" permissions: contents: write @@ -18,11 +18,16 @@ jobs: - uses: actions/checkout@v3 with: fetch-depth: 0 + - name: 🐋 Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - run: git fetch --force --tags - run: echo "Ref name ${{github.ref_name}}" - uses: actions/setup-go@v3 with: - go-version: '>=1.19.3' + go-version: ">=1.19.3" cache: true cache-dependency-path: cli/go.sum - name: libssl1.1 => libssl1.0-dev for OSXCross @@ -45,8 +50,7 @@ jobs: AUR_KEY: ${{ secrets.AUR_KEY }} - uses: actions/setup-python@v4 - run: pip install --upgrade cloudsmith-cli - - name: Publish to CloudSmith + - name: Publish to CloudSmith run: sh cli/upload_to_cloudsmith.sh env: CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }} - diff --git a/.goreleaser.yaml b/.goreleaser.yaml index fc39224aa..8e9c575d9 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -68,10 +68,10 @@ archives: release: replace_existing_draft: true - mode: 'replace' + mode: "replace" checksum: - name_template: 'checksums.txt' + name_template: "checksums.txt" snapshot: name_template: "{{ incpatch .Version }}-devel" @@ -80,8 +80,8 @@ changelog: sort: asc filters: exclude: - - '^docs:' - - '^test:' + - "^docs:" + - "^test:" # publishers: # - name: fury.io @@ -109,30 +109,30 @@ brews: man1.install "manpages/infisical.1.gz" nfpms: -- id: infisical - package_name: infisical - builds: - - all-other-builds - vendor: Infisical, Inc - homepage: https://infisical.com/ - maintainer: Infisical, Inc - description: The offical Infisical CLI - license: MIT - formats: - - rpm - - deb - - apk - - archlinux - bindir: /usr/bin - contents: - - src: ./completions/infisical.bash - dst: /etc/bash_completion.d/infisical - - src: ./completions/infisical.fish - dst: /usr/share/fish/vendor_completions.d/infisical.fish - - src: ./completions/infisical.zsh - dst: /usr/share/zsh/site-functions/_infisical - - src: ./manpages/infisical.1.gz - dst: /usr/share/man/man1/infisical.1.gz + - id: infisical + package_name: infisical + builds: + - all-other-builds + vendor: Infisical, Inc + homepage: https://infisical.com/ + maintainer: Infisical, Inc + description: The offical Infisical CLI + license: MIT + formats: + - rpm + - deb + - apk + - archlinux + bindir: /usr/bin + contents: + - src: ./completions/infisical.bash + dst: /etc/bash_completion.d/infisical + - src: ./completions/infisical.fish + dst: /usr/share/fish/vendor_completions.d/infisical.fish + - src: ./completions/infisical.zsh + dst: /usr/share/zsh/site-functions/_infisical + - src: ./manpages/infisical.1.gz + dst: /usr/share/man/man1/infisical.1.gz scoop: bucket: @@ -146,15 +146,14 @@ scoop: license: MIT aurs: - - - name: infisical-bin + - name: infisical-bin homepage: "https://infisical.com" description: "The official Infisical CLI" maintainers: - Infisical, Inc license: MIT - private_key: '{{ .Env.AUR_KEY }}' - git_url: 'ssh://aur@aur.archlinux.org/infisical-bin.git' + private_key: "{{ .Env.AUR_KEY }}" + git_url: "ssh://aur@aur.archlinux.org/infisical-bin.git" package: |- # bin install -Dm755 "./infisical" "${pkgdir}/usr/bin/infisical" @@ -169,19 +168,12 @@ aurs: install -Dm644 "./completions/infisical.fish" "${pkgdir}/usr/share/fish/vendor_completions.d/infisical.fish" # man pages install -Dm644 "./manpages/infisical.1.gz" "${pkgdir}/usr/share/man/man1/infisical.1.gz" -# dockers: -# - dockerfile: goreleaser.dockerfile -# goos: linux -# goarch: amd64 -# ids: -# - infisical -# image_templates: -# - "infisical/cli:{{ .Version }}" -# - "infisical/cli:{{ .Major }}.{{ .Minor }}" -# - "infisical/cli:{{ .Major }}" -# - "infisical/cli:latest" -# build_flag_templates: -# - "--label=org.label-schema.schema-version=1.0" -# - "--label=org.label-schema.version={{.Version}}" -# - "--label=org.label-schema.name={{.ProjectName}}" -# - "--platform=linux/amd64" \ No newline at end of file +dockers: + - dockerfile: cli/docker/Dockerfile + goos: linux + goarch: amd64 + ids: + - infisical + image_templates: + - "infisical/cli:{{ .Version }}" + - "infisical/cli:latest" diff --git a/cli/docker/Dockerfile b/cli/docker/Dockerfile new file mode 100644 index 000000000..0436d4d8e --- /dev/null +++ b/cli/docker/Dockerfile @@ -0,0 +1,4 @@ +FROM alpine +RUN apk add --no-cache tini +COPY infisical /bin/infisical +ENTRYPOINT ["/sbin/tini", "--", "/bin/infisical"] \ No newline at end of file From 17f9e53779ef7c7c2edbf5b6f66cc37c465e8f41 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Sun, 12 Feb 2023 17:54:22 -0800 Subject: [PATCH 7/9] Updated the dashabord, members, and settings pages --- .../controllers/v1/organizationController.ts | 30 ++++++------ .../controllers/v2/environmentController.ts | 5 +- frontend/public/data/frequentInterfaces.ts | 2 +- .../basic/table/ProjectUsersTable.tsx | 45 ++++++++++++----- .../src/components/basic/table/UserTable.tsx | 21 +++++++- .../context/Notifications/Notification.tsx | 8 +-- .../dashboard/DashboardInputField.tsx | 41 ++++++++-------- frontend/src/components/dashboard/KeyPair.tsx | 18 ++++++- frontend/src/components/dashboard/SideBar.tsx | 12 ++--- .../utilities/secrets/encryptSecrets.ts | 2 +- .../utilities/secrets/getSecretsForProject.ts | 19 ++++--- .../src/components/v2/Popover/Popover.tsx | 49 +++++++++++++++++++ frontend/src/components/v2/Popover/index.tsx | 2 + frontend/src/components/v2/Select/Select.tsx | 13 +++-- .../src/ee/components/PITRecoverySidebar.tsx | 7 +-- .../src/ee/components/SecretVersionList.tsx | 12 ++--- .../organization/GetOrgProjectMemberships.ts | 24 +++++++++ frontend/src/pages/dashboard/[id].tsx | 36 +++++++++----- frontend/src/pages/settings/org/[id].tsx | 2 +- frontend/src/pages/settings/personal/[id].tsx | 2 +- 20 files changed, 252 insertions(+), 98 deletions(-) create mode 100644 frontend/src/components/v2/Popover/Popover.tsx create mode 100644 frontend/src/components/v2/Popover/index.tsx create mode 100644 frontend/src/pages/api/organization/GetOrgProjectMemberships.ts diff --git a/backend/src/controllers/v1/organizationController.ts b/backend/src/controllers/v1/organizationController.ts index eaf58fad7..66326e560 100644 --- a/backend/src/controllers/v1/organizationController.ts +++ b/backend/src/controllers/v1/organizationController.ts @@ -397,9 +397,21 @@ export const getOrganizationMembersAndTheirWorkspaces = async ( res: Response ) => { const { organizationId } = req.params; - const orgMemberships = await MembershipOrg.find({ organization: organizationId }); - const userIds = orgMemberships.map(orgMembership => orgMembership.user); - const memberships = await Membership.find({ user: { $in: userIds } }); + + const workspacesSet = ( + await Workspace.find( + { + organization: organizationId + }, + '_id' + ) + ).map((w) => w._id.toString()); + + const memberships = ( + await Membership.find({ + workspace: { $in: workspacesSet } + }).populate('workspace') + ); const userToWorkspaceIds: any = {}; memberships.forEach(membership => { @@ -411,15 +423,5 @@ export const getOrganizationMembersAndTheirWorkspaces = async ( } }); - const workspaceIds = Object.values(userToWorkspaceIds).flat() - const workspacesList = await Workspace.find({ - organization: organizationId, - _id: { $in: workspaceIds } - }); - - const populatedUserWorkspaces = _.mapValues(userToWorkspaceIds, workspaceIds => - _.map(workspaceIds, id => _.find(workspacesList, { _id: id })) - ); - - return res.json(populatedUserWorkspaces); + return res.json(userToWorkspaceIds); }; \ No newline at end of file diff --git a/backend/src/controllers/v2/environmentController.ts b/backend/src/controllers/v2/environmentController.ts index 7a0d5e1c5..b82dca9fe 100644 --- a/backend/src/controllers/v2/environmentController.ts +++ b/backend/src/controllers/v2/environmentController.ts @@ -246,13 +246,14 @@ export const getAllAccessibleEnvironmentsOfWorkspace = async ( relatedWorkspace.environments.forEach(environment => { const isReadBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: ABILITY_READ }) const isWriteBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: ABILITY_WRITE }) - if (isReadBlocked) { + if (isReadBlocked && isWriteBlocked) { return } else { accessibleEnvironments.push({ name: environment.name, slug: environment.slug, - isWriteDenied: isWriteBlocked + isWriteDenied: isWriteBlocked, + isReadDenied: isReadBlocked }) } }) diff --git a/frontend/public/data/frequentInterfaces.ts b/frontend/public/data/frequentInterfaces.ts index 9865d9909..fa6c73a57 100644 --- a/frontend/public/data/frequentInterfaces.ts +++ b/frontend/public/data/frequentInterfaces.ts @@ -10,7 +10,7 @@ export interface Tag { export interface SecretDataProps { pos: number; key: string; - value: string; + value: string | undefined; valueOverride: string | undefined; id: string; comment: string; diff --git a/frontend/src/components/basic/table/ProjectUsersTable.tsx b/frontend/src/components/basic/table/ProjectUsersTable.tsx index 9d278bdc4..27346195c 100644 --- a/frontend/src/components/basic/table/ProjectUsersTable.tsx +++ b/frontend/src/components/basic/table/ProjectUsersTable.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; import { useRouter } from 'next/router'; -import { faX } from '@fortawesome/free-solid-svg-icons'; +import { faEye, faEyeSlash, faPenToSquare, faPlus, faX } from '@fortawesome/free-solid-svg-icons'; import { plans } from 'public/data/frequentConstants'; import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider'; @@ -106,6 +106,11 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => { ability: "read", environmentSlug: slug }]; + } else if (val === "Add Only") { + denials = [{ + ability: "read", + environmentSlug: slug + }]; } else { denials = []; } @@ -185,21 +190,21 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => { return (
-
+
- + {workspaceEnvs.map(env => ( - ))} @@ -221,7 +226,7 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => { user.email?.toLowerCase().includes(filter) ) .map((row, index) => ( - + @@ -231,7 +236,8 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => { - {workspaceEnvs.map((env) => )}
NAME EMAIL ROLE - {env.name.toUpperCase()}
+
+ {env.slug.toUpperCase()}
{/* PERMISSION */}
{row.firstName} {row.lastName}
+ {workspaceEnvs.map((env) => diff --git a/frontend/src/components/basic/table/UserTable.tsx b/frontend/src/components/basic/table/UserTable.tsx index 02e65c547..c14d77b08 100644 --- a/frontend/src/components/basic/table/UserTable.tsx +++ b/frontend/src/components/basic/table/UserTable.tsx @@ -4,6 +4,7 @@ import { faX } from '@fortawesome/free-solid-svg-icons'; import changeUserRoleInOrganization from '@app/pages/api/organization/changeUserRoleInOrganization'; import deleteUserFromOrganization from '@app/pages/api/organization/deleteUserFromOrganization'; +import getOrganizationProjectMemberships from '@app/pages/api/organization/GetOrgProjectMemberships'; import deleteUserFromWorkspace from '@app/pages/api/workspace/deleteUserFromWorkspace'; import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey'; import uploadKeys from '@app/pages/api/workspace/uploadKeys'; @@ -36,6 +37,8 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg } ); const router = useRouter(); const [myRole, setMyRole] = useState('member'); + const [userProjectMemberships, setUserProjectMemberships] = useState([]); + console.log(123, userData) const workspaceId = router.query.id as string; // Delete the row in the table (e.g. a user) @@ -79,6 +82,10 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg } useEffect(() => { setMyRole(userData.filter((user) => user.email === myUser)[0]?.role); + (async () => { + const result = await getOrganizationProjectMemberships({ orgId: String(localStorage.getItem("orgData.id"))}) + setUserProjectMemberships(result); + })(); }, [userData, myUser]); const grantAccess = async (id: string, publicKey: string) => { @@ -110,7 +117,7 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg } }; return ( -
+
@@ -118,6 +125,7 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg } + @@ -189,6 +197,17 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg } )} + +
NAME EMAIL ROLEPROJECTS
+ + {userProjectMemberships[row.userId] + ? userProjectMemberships[row.userId]?.map((project: any) => ( +
+ {project.name} +
+ )) + : This user isn't part of any projects yet.} +
{myUser !== row.email && // row.role !== "admin" && diff --git a/frontend/src/components/context/Notifications/Notification.tsx b/frontend/src/components/context/Notifications/Notification.tsx index ca1b155bd..921f86dec 100644 --- a/frontend/src/components/context/Notifications/Notification.tsx +++ b/frontend/src/components/context/Notifications/Notification.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef } from 'react'; -import { faX } from '@fortawesome/free-solid-svg-icons'; +import { faXmark } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; type NotificationType = 'success' | 'error' | 'info'; @@ -36,7 +36,7 @@ const Notification = ({ notification, clearNotification }: NotificationProps) => return (
{notification.type === 'error' && ( @@ -48,13 +48,13 @@ const Notification = ({ notification, clearNotification }: NotificationProps) => {notification.type === 'info' && (
)} -

{notification.text}

+

{notification.text}

); diff --git a/frontend/src/components/dashboard/DashboardInputField.tsx b/frontend/src/components/dashboard/DashboardInputField.tsx index a55c3ba61..a5ee0ed3a 100644 --- a/frontend/src/components/dashboard/DashboardInputField.tsx +++ b/frontend/src/components/dashboard/DashboardInputField.tsx @@ -1,9 +1,10 @@ import { memo, SyntheticEvent, useRef } from 'react'; -import { faCircle, faExclamationCircle, faEye, faLayerGroup } from '@fortawesome/free-solid-svg-icons'; +import { faCircle, faCodeBranch, faExclamationCircle, faEye } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import guidGenerator from '../utilities/randomId'; import { HoverObject } from '../v2/HoverCard'; +import { PopoverObject } from '../v2/Popover/Popover'; const REGEX = /([$]{.*?})/g; @@ -112,7 +113,7 @@ const DashboardInputField = ({ }}> @@ -125,24 +126,24 @@ const DashboardInputField = ({ const error = startsWithNumber || isDuplicate; return ( -
-
- onChangeHandler(e.target.value, position)} - type={type} - value={value} - className='z-10 peer ph-no-capture bg-transparent py-2.5 caret-bunker-200 text-sm px-2 w-full min-w-16 outline-none text-bunker-300 focus:text-bunker-100 placeholder:text-bunker-400 placeholder:focus:text-transparent placeholder duration-200' - spellCheck="false" - placeholder='–' - /> + +
+
+ {value?.split("\n")[0] ? + {value?.split("\n")[0]} + : - } + {value?.split("\n")[1] && + {value?.split("\n")[1]} + } +
-
+ ); } if (type === 'value') { @@ -215,7 +216,7 @@ const DashboardInputField = ({ ))} {value?.split('').length === 0 && EMPTY}
-
+
)} diff --git a/frontend/src/components/dashboard/KeyPair.tsx b/frontend/src/components/dashboard/KeyPair.tsx index 711d25a51..de55b7322 100644 --- a/frontend/src/components/dashboard/KeyPair.tsx +++ b/frontend/src/components/dashboard/KeyPair.tsx @@ -132,7 +132,7 @@ const KeyPair = ({ /> -
+
- { if (deleteRow) { deleteRow({ ids: [keyPair.id], secretName: keyPair?.key }) }}} isPlain /> + :
+
null} + role="button" + tabIndex={0} + onClick={() => { if (deleteRow) { + deleteRow({ ids: [keyPair.id], secretName: keyPair?.key }) + }}} + className="invisible group-hover:visible" + > + +
+
}
diff --git a/frontend/src/components/dashboard/SideBar.tsx b/frontend/src/components/dashboard/SideBar.tsx index 1d0996376..6fac52ed7 100644 --- a/frontend/src/components/dashboard/SideBar.tsx +++ b/frontend/src/components/dashboard/SideBar.tsx @@ -18,7 +18,7 @@ import GenerateSecretMenu from './GenerateSecretMenu'; interface SecretProps { key: string; - value: string; + value: string | undefined; valueOverride: string | undefined; pos: number; id: string; @@ -80,9 +80,9 @@ const SideBar = ({ const { t } = useTranslation(); return ( -
+
{isLoading ? ( -
+
) : ( -
+

{t('dashboard:sidebar.secret')}

)} -
+