Compare commits

...

86 Commits

Author SHA1 Message Date
f799e224a0 use RequestError instead of Error for bot 2023-12-13 18:22:29 -05:00
8a87277fe6 parse bot not found in agent 2023-12-13 18:07:39 -05:00
32805c726a add docs for uni auth in agent 2023-12-13 17:27:30 -05:00
6c4a6d31e4 Merge pull request #1229 from Infisical/identities-docs
Update Identities Documentation + related API Reference Items
2023-12-13 16:57:13 -05:00
e7b89b645f Merge branch 'main' into identities-docs 2023-12-13 16:56:35 -05:00
b60cf2eb07 make minor updates to auth docs 2023-12-13 16:52:57 -05:00
cf5a79995f revert defaults to 30 days 2023-12-13 16:52:23 -05:00
c51f09fd3a Merge pull request #1241 from Infisical/patch-3
sync package.lock frontend
2023-12-13 14:45:48 -05:00
f9444c5205 sync package.lock frontend 2023-12-13 14:31:10 -05:00
7dd0943b2d remove sleep from template engine agent 2023-12-13 14:19:30 -05:00
31a9f032b3 Merge pull request #1236 from akhilmhdh/feat/bring-back-secret-index
feat: brought back secret indexing popup in overview page
2023-12-13 12:59:37 -05:00
9c55d1906d Merge pull request #1239 from Infisical/workspace-key-log
add workspace id and receiver to getWorkspaceKey error
2023-12-13 11:28:14 -05:00
ff54a20ace add workspace id and receiver to getWorkspaceKey error 2023-12-13 11:22:10 -05:00
8bf7eba07b fix: show popup only for admins 2023-12-13 11:55:44 +05:30
bb75ea550a prevent access token ttl=0 2023-12-12 22:17:46 -05:00
344f7276d2 update agent command description 2023-12-12 21:55:41 -05:00
c375662411 Merge pull request #1238 from Infisical/add-universal-auth-to-agent
add universal auth to agent
2023-12-12 20:36:16 -05:00
cc4ad1df4b update docs for agent 2023-12-12 20:24:17 -05:00
c92c0f7288 add universal auth to agent 2023-12-12 19:36:48 -05:00
fbe0cf006f add max ttl to renew and login api responses 2023-12-12 19:35:45 -05:00
d2f959558e fix: resolved recursion issue in select 2023-12-12 22:29:38 +05:30
e50c89e326 feat: brought back secret indexing popup in overview page 2023-12-12 21:03:47 +05:30
6cda14328b Update getting started guide for fetching secrets via API 2023-12-12 17:59:56 +07:00
b551ee50e7 Fix merge conflicts 2023-12-12 15:50:14 +07:00
93aeacc6b6 Add API reference docs for identity / universal auth endpoints 2023-12-12 13:42:17 +07:00
f940f8b79d remove unused methods in cli 2023-12-11 16:52:47 -05:00
72ac2c04b8 Merge pull request #1228 from rawkode/fix/injecting-breaks-env
fix: "Injecting..." status string can be omitted by log levels
2023-12-11 16:41:58 -05:00
bb3d591f21 remove cli update notification delay 2023-12-11 15:14:49 -05:00
763ce1b206 Merge pull request #1230 from Infisical/non-zero-max-ttl
non-zero-max-ttl
2023-12-11 14:39:18 -05:00
1f97ac5192 non-zero-max-ttl 2023-12-11 14:21:51 -05:00
5f29562fad Update existing endpoints in API reference to support Identities, update Identities docs 2023-12-11 20:01:32 +07:00
f3e8ef1537 Merge pull request #1192 from Infisical/stv3-org-roles
Add Identities + Universal Auth Authentication Method
2023-12-10 16:57:39 -05:00
544d37bbc4 fix: "Injecting..." status stirng can be omitted by log levels
When using `infisical run`, I am often running another command
that needs to be processed or consumed by another; such as:

infisical run -- supabase status -o env

The Injecting string was being printed directly to stdout and
stopping such scripting from being successful, without further
adding tail -n+2.

This change defaults the output to the INFO logging level, which
means the behaviour is the exact same for everything; however
those who wish can omit this output with -l error|fatal
2023-12-10 16:13:38 +00:00
4f6adb50d1 Minor UX update to identities 2023-12-10 22:35:12 +07:00
444ce9508d Resolve PR review items, moved identity auth logic into separate controller, etc. 2023-12-10 14:15:25 +07:00
aabd896c37 Updated changelog 2023-12-09 16:58:16 -08:00
50ef23e8a0 Restructure MIs to more generic Identity 2023-12-09 22:18:38 +07:00
b87f51a044 Update Chart.yaml 2023-12-08 17:26:19 -05:00
1233d9c1a0 Merge pull request #1223 from Infisical/patch-k8s-dependency-vulnerability
update resty + patch kube-proxy
2023-12-08 17:25:55 -05:00
ff0b4d7f2b Merge pull request #1225 from Infisical/upgrade-axios
Address axios vulnerability
2023-12-08 17:25:19 -05:00
ef61bc6a40 upgrade axios 2023-12-08 16:26:42 -05:00
13ee8c4e13 Merge pull request #1224 from Infisical/resolve-x/net-vulnerability
Update Resty
2023-12-08 16:01:06 -05:00
6ea9fc7134 update resty 2023-12-08 15:49:48 -05:00
89d0c0e3c3 temporarily disable max access token ttl 2023-12-06 20:45:07 -05:00
a4f6b828ad fix update machine params + default to no max ttl 2023-12-06 20:35:26 -05:00
0fb2056b8b update delete client secret to revoke client secret 2023-12-06 18:16:14 -05:00
ec5cf97f18 Add case for MI token renewal 2023-12-07 00:29:08 +07:00
69b57817d6 Switch access token tracking to be persistent, add num uses, draft token renewal, update docs 2023-12-07 00:11:16 +07:00
aafbe40c02 add machineIdentityAccessToken model 2023-12-05 16:58:21 -05:00
9d9b83f909 fix expired client secret logic 2023-12-05 16:38:43 -05:00
ea1f144b54 add index to machine identity model 2023-12-05 16:37:16 -05:00
591f33ffbe Move MI endpoints from v3 to v1 2023-12-05 22:24:25 +07:00
855158d0bb Allow MI to create another MI test 2023-12-05 19:34:43 +07:00
87e997e7a0 Replace most getUserOrgPermissions with more generic getAuthDataOrgPermissions for MIs in backend 2023-12-05 19:20:30 +07:00
3c449214d1 Add error messages for MI expired client secret, num uses limit reached 2023-12-05 17:53:35 +07:00
d813f0716f Switch RBAC flag 2023-12-05 17:47:46 +07:00
6787c0eaaa Update authz logic for MI 2023-12-05 17:46:50 +07:00
c91f6521c1 Update MI fields numUses, numUsesLimit, ttl, added modal for delete client secret confirmation 2023-12-05 11:15:57 +07:00
0ebd1d3d81 Merge branch 'stv3-org-roles' of https://github.com/Infisical/infisical into stv3-org-roles 2023-12-05 08:57:50 +07:00
d257a449bb add compound index to machineIdentityClientSecretDataSchema 2023-12-04 19:42:09 -05:00
6a744c96e5 add index to workspace to improve query 2023-12-04 19:41:40 -05:00
28b617fd89 Update MI docs for client id/secret 2023-12-04 23:04:04 +07:00
8b1eaad7b5 Fix audit logs UI rendering 2023-12-04 18:23:04 +07:00
c917cf8a18 Add logging to MI secret endpoints 2023-12-04 17:48:32 +07:00
282830e7a2 Fix lint issues 2023-12-04 16:24:55 +07:00
3d6f04b94e Merge remote-tracking branch 'origin' into stv3-org-roles 2023-12-04 16:15:14 +07:00
60a5092947 Merge remote-tracking branch 'origin' into stv3-org-roles 2023-12-04 16:14:31 +07:00
69dae1f0b2 Move MI from refresh token to client id / client secrets approach 2023-12-04 16:13:00 +07:00
6557d7668e Add docs for MIs 2023-11-29 15:55:15 +07:00
77e3d10a64 Flip RBAC 2023-11-27 15:50:59 +07:00
814b71052d Update error-handling, show underprivileged notification error 2023-11-27 15:50:00 +07:00
6579b3c93f Update MI authz logic 2023-11-27 14:19:10 +07:00
99c41bb63b Add no access role, replace ST V3 refs with machine 2023-11-27 09:59:15 +07:00
63df0dba64 Add default org and project-level no access roles 2023-11-26 17:02:03 +07:00
4e050cfe7a Fix frontend lint issues 2023-11-26 13:34:50 +07:00
32f5c96dd2 Move custom role paywall to assignment step 2023-11-26 13:18:49 +07:00
5b923c25b5 Added authz logic to MI 2023-11-25 18:37:20 +07:00
29016fbb23 Fix populate service in getAuthDataProjectPermissions 2023-11-24 20:10:50 +07:00
0c0139ac8f Restyle project members page 2023-11-24 20:00:40 +07:00
180274be34 Add endpoint to update MI project-level roles 2023-11-24 19:50:01 +07:00
595a26a366 Update ST V3 to machine identity 2023-11-24 19:19:31 +07:00
41c41a647f Standardize org members page styling 2023-11-24 14:09:56 +07:00
c3d2b7d3fc Pull main 2023-11-23 18:00:07 +07:00
87984a704a Fix merge conflicts 2023-11-23 15:37:14 +07:00
33e4104e98 Fix merge conflicts 2023-11-23 15:36:19 +07:00
597e1e1ca8 Continue ST V3 roles 2023-11-23 11:56:56 +07:00
218 changed files with 19436 additions and 3964 deletions

View File

@ -24,7 +24,7 @@
"ajv": "^8.12.0", "ajv": "^8.12.0",
"argon2": "^0.30.3", "argon2": "^0.30.3",
"aws-sdk": "^2.1364.0", "aws-sdk": "^2.1364.0",
"axios": "^1.3.5", "axios": "^1.6.0",
"axios-retry": "^3.4.0", "axios-retry": "^3.4.0",
"bcrypt": "^5.1.0", "bcrypt": "^5.1.0",
"bigint-conversion": "^2.4.0", "bigint-conversion": "^2.4.0",
@ -8325,9 +8325,9 @@
} }
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.6.2", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz",
"integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", "integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.0", "follow-redirects": "^1.15.0",
"form-data": "^4.0.0", "form-data": "^4.0.0",
@ -25250,9 +25250,9 @@
} }
}, },
"axios": { "axios": {
"version": "1.6.2", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz",
"integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", "integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==",
"requires": { "requires": {
"follow-redirects": "^1.15.0", "follow-redirects": "^1.15.0",
"form-data": "^4.0.0", "form-data": "^4.0.0",

View File

@ -15,7 +15,7 @@
"ajv": "^8.12.0", "ajv": "^8.12.0",
"argon2": "^0.30.3", "argon2": "^0.30.3",
"aws-sdk": "^2.1364.0", "aws-sdk": "^2.1364.0",
"axios": "^1.3.5", "axios": "^1.6.0",
"axios-retry": "^3.4.0", "axios-retry": "^3.4.0",
"bcrypt": "^5.1.0", "bcrypt": "^5.1.0",
"bigint-conversion": "^2.4.0", "bigint-conversion": "^2.4.0",

File diff suppressed because it is too large Load Diff

View File

@ -3,15 +3,22 @@ import jwt from "jsonwebtoken";
import * as bigintConversion from "bigint-conversion"; import * as bigintConversion from "bigint-conversion";
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const jsrp = require("jsrp"); const jsrp = require("jsrp");
import { LoginSRPDetail, TokenVersion, User } from "../../models"; import {
LoginSRPDetail,
TokenVersion,
User
} from "../../models";
import { clearTokens, createToken, issueAuthTokens } from "../../helpers/auth"; import { clearTokens, createToken, issueAuthTokens } from "../../helpers/auth";
import { checkUserDevice } from "../../helpers/user"; import { checkUserDevice } from "../../helpers/user";
import { AuthTokenType } from "../../variables"; import { AuthTokenType } from "../../variables";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; import {
BadRequestError,
UnauthorizedRequestError
} from "../../utils/errors";
import { import {
getAuthSecret, getAuthSecret,
getHttpsEnabled, getHttpsEnabled,
getJwtAuthLifetime getJwtAuthLifetime,
} from "../../config"; } from "../../config";
import { ActorType } from "../../ee/models"; import { ActorType } from "../../ee/models";
import { validateRequest } from "../../helpers/validation"; import { validateRequest } from "../../helpers/validation";
@ -25,10 +32,11 @@ declare module "jsonwebtoken" {
userId: string; userId: string;
refreshVersion?: number; refreshVersion?: number;
} }
export interface ServiceRefreshTokenJwtPayload extends jwt.JwtPayload { export interface IdentityAccessTokenJwtPayload extends jwt.JwtPayload {
serviceTokenDataId: string; _id: string;
clientSecretId: string;
identityAccessTokenId: string;
authTokenType: string; authTokenType: string;
tokenVersion: number;
} }
} }

View File

@ -1,4 +1,5 @@
import * as authController from "./authController"; import * as authController from "./authController";
import * as universalAuthController from "./universalAuthController";
import * as botController from "./botController"; import * as botController from "./botController";
import * as integrationAuthController from "./integrationAuthController"; import * as integrationAuthController from "./integrationAuthController";
import * as integrationController from "./integrationController"; import * as integrationController from "./integrationController";
@ -20,6 +21,7 @@ import * as adminController from "./adminController";
export { export {
authController, authController,
universalAuthController,
botController, botController,
integrationAuthController, integrationAuthController,
integrationController, integrationController,

View File

@ -4,9 +4,9 @@ import { IUser, Key, Membership, MembershipOrg, User, Workspace } from "../../mo
import { EventType, Role } from "../../ee/models"; import { EventType, Role } from "../../ee/models";
import { deleteMembership as deleteMember, findMembership } from "../../helpers/membership"; import { deleteMembership as deleteMember, findMembership } from "../../helpers/membership";
import { sendMail } from "../../helpers/nodemailer"; import { sendMail } from "../../helpers/nodemailer";
import { ACCEPTED, ADMIN, CUSTOM, MEMBER, VIEWER } from "../../variables"; import { ACCEPTED, ADMIN, CUSTOM, MEMBER, NO_ACCESS, VIEWER } from "../../variables";
import { getSiteURL } from "../../config"; import { getSiteURL } from "../../config";
import { EEAuditLogService } from "../../ee/services"; import { EEAuditLogService, EELicenseService } from "../../ee/services";
import { validateRequest } from "../../helpers/validation"; import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/membership"; import * as reqValidator from "../../validation/membership";
import { import {
@ -129,7 +129,7 @@ export const changeMembershipRole = async (req: Request, res: Response) => {
ProjectPermissionSub.Member ProjectPermissionSub.Member
); );
const isCustomRole = ![ADMIN, MEMBER, VIEWER].includes(role); const isCustomRole = ![ADMIN, MEMBER, VIEWER, NO_ACCESS].includes(role);
if (isCustomRole) { if (isCustomRole) {
const wsRole = await Role.findOne({ const wsRole = await Role.findOne({
slug: role, slug: role,
@ -137,6 +137,13 @@ export const changeMembershipRole = async (req: Request, res: Response) => {
workspace: membershipToChangeRole.workspace workspace: membershipToChangeRole.workspace
}); });
if (!wsRole) throw BadRequestError({ message: "Role not found" }); if (!wsRole) throw BadRequestError({ message: "Role not found" });
const plan = await EELicenseService.getPlan(wsRole.organization);
if (!plan.rbac) return res.status(400).send({
message: "Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."
});
const membership = await Membership.findByIdAndUpdate(membershipId, { const membership = await Membership.findByIdAndUpdate(membershipId, {
role: CUSTOM, role: CUSTOM,
customRole: wsRole customRole: wsRole

View File

@ -21,7 +21,7 @@ import { validateRequest } from "../../helpers/validation";
import { import {
OrgPermissionActions, OrgPermissionActions,
OrgPermissionSubjects, OrgPermissionSubjects,
getUserOrgPermissions getAuthDataOrgPermissions
} from "../../ee/services/RoleService"; } from "../../ee/services/RoleService";
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
@ -45,10 +45,11 @@ export const deleteMembershipOrg = async (req: Request, _res: Response) => {
throw new Error("Failed to delete organization membership that doesn't exist"); throw new Error("Failed to delete organization membership that doesn't exist");
} }
const { permission, membership: membershipOrg } = await getUserOrgPermissions( const { permission } = await getAuthDataOrgPermissions({
req.user._id, authData: req.authData,
membershipOrgToDelete.organization.toString() organizationId: membershipOrgToDelete.organization
); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Delete, OrgPermissionActions.Delete,
OrgPermissionSubjects.Member OrgPermissionSubjects.Member
@ -60,7 +61,7 @@ export const deleteMembershipOrg = async (req: Request, _res: Response) => {
}); });
await updateSubscriptionOrgQuantity({ await updateSubscriptionOrgQuantity({
organizationId: membershipOrg.organization.toString() organizationId: membershipOrgToDelete.organization.toString()
}); });
return membershipOrgToDelete; return membershipOrgToDelete;
@ -96,7 +97,11 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
body: { inviteeEmail, organizationId } body: { inviteeEmail, organizationId }
} = await validateRequest(reqValidator.InviteUserToOrgv1, req); } = await validateRequest(reqValidator.InviteUserToOrgv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create, OrgPermissionActions.Create,
OrgPermissionSubjects.Member OrgPermissionSubjects.Member

View File

@ -1,4 +1,5 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import { Types } from "mongoose";
import { import {
IncidentContactOrg, IncidentContactOrg,
Membership, Membership,
@ -14,7 +15,7 @@ import { ACCEPTED } from "../../variables";
import { import {
OrgPermissionActions, OrgPermissionActions,
OrgPermissionSubjects, OrgPermissionSubjects,
getUserOrgPermissions getAuthDataOrgPermissions
} from "../../ee/services/RoleService"; } from "../../ee/services/RoleService";
import { OrganizationNotFoundError } from "../../utils/errors"; import { OrganizationNotFoundError } from "../../utils/errors";
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
@ -44,7 +45,10 @@ export const getOrganization = async (req: Request, res: Response) => {
} = await validateRequest(reqValidator.GetOrgv1, req); } = await validateRequest(reqValidator.GetOrgv1, req);
// ensure user has membership // ensure user has membership
await getUserOrgPermissions(req.user._id, organizationId); await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
})
const organization = await Organization.findById(organizationId); const organization = await Organization.findById(organizationId);
if (!organization) { if (!organization) {
@ -69,7 +73,11 @@ export const getOrganizationMembers = async (req: Request, res: Response) => {
params: { organizationId } params: { organizationId }
} = await validateRequest(reqValidator.GetOrgMembersv1, req); } = await validateRequest(reqValidator.GetOrgMembersv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read, OrgPermissionActions.Read,
OrgPermissionSubjects.Member OrgPermissionSubjects.Member
@ -95,7 +103,10 @@ export const getOrganizationWorkspaces = async (req: Request, res: Response) =>
params: { organizationId } params: { organizationId }
} = await validateRequest(reqValidator.GetOrgWorkspacesv1, req); } = await validateRequest(reqValidator.GetOrgWorkspacesv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
})
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read, OrgPermissionActions.Read,
OrgPermissionSubjects.Workspace OrgPermissionSubjects.Workspace
@ -137,7 +148,10 @@ export const changeOrganizationName = async (req: Request, res: Response) => {
body: { name } body: { name }
} = await validateRequest(reqValidator.ChangeOrgNamev1, req); } = await validateRequest(reqValidator.ChangeOrgNamev1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit, OrgPermissionActions.Edit,
OrgPermissionSubjects.Settings OrgPermissionSubjects.Settings
@ -172,7 +186,10 @@ export const getOrganizationIncidentContacts = async (req: Request, res: Respons
params: { organizationId } params: { organizationId }
} = await validateRequest(reqValidator.GetOrgIncidentContactv1, req); } = await validateRequest(reqValidator.GetOrgIncidentContactv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read, OrgPermissionActions.Read,
OrgPermissionSubjects.IncidentAccount OrgPermissionSubjects.IncidentAccount
@ -199,7 +216,10 @@ export const addOrganizationIncidentContact = async (req: Request, res: Response
body: { email } body: { email }
} = await validateRequest(reqValidator.CreateOrgIncideContact, req); } = await validateRequest(reqValidator.CreateOrgIncideContact, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create, OrgPermissionActions.Create,
OrgPermissionSubjects.IncidentAccount OrgPermissionSubjects.IncidentAccount
@ -228,7 +248,10 @@ export const deleteOrganizationIncidentContact = async (req: Request, res: Respo
body: { email } body: { email }
} = await validateRequest(reqValidator.DelOrgIncideContact, req); } = await validateRequest(reqValidator.DelOrgIncideContact, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Delete, OrgPermissionActions.Delete,
OrgPermissionSubjects.IncidentAccount OrgPermissionSubjects.IncidentAccount
@ -257,7 +280,10 @@ export const createOrganizationPortalSession = async (req: Request, res: Respons
params: { organizationId } params: { organizationId }
} = await validateRequest(reqValidator.GetOrgPlanBillingInfov1, req); } = await validateRequest(reqValidator.GetOrgPlanBillingInfov1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit, OrgPermissionActions.Edit,
OrgPermissionSubjects.Billing OrgPermissionSubjects.Billing
@ -321,7 +347,10 @@ export const getOrganizationMembersAndTheirWorkspaces = async (req: Request, res
params: { organizationId } params: { organizationId }
} = await validateRequest(reqValidator.GetOrgMembersv1, req); } = await validateRequest(reqValidator.GetOrgMembersv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read, OrgPermissionActions.Read,
OrgPermissionSubjects.Member OrgPermissionSubjects.Member

View File

@ -21,7 +21,7 @@ import * as reqValidator from "../../validation/secretScanning";
import { import {
OrgPermissionActions, OrgPermissionActions,
OrgPermissionSubjects, OrgPermissionSubjects,
getUserOrgPermissions getAuthDataOrgPermissions
} from "../../ee/services/RoleService"; } from "../../ee/services/RoleService";
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
@ -38,7 +38,10 @@ export const createInstallationSession = async (req: Request, res: Response) =>
}); });
} }
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create, OrgPermissionActions.Create,
OrgPermissionSubjects.SecretScanning OrgPermissionSubjects.SecretScanning
@ -71,10 +74,11 @@ export const linkInstallationToOrganization = async (req: Request, res: Response
throw UnauthorizedRequestError(); throw UnauthorizedRequestError();
} }
const { permission } = await getUserOrgPermissions( const { permission } = await getAuthDataOrgPermissions({
req.user._id, authData: req.authData,
installationSession.organization.toString() organizationId: installationSession.organization
); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit, OrgPermissionActions.Edit,
OrgPermissionSubjects.SecretScanning OrgPermissionSubjects.SecretScanning
@ -142,7 +146,10 @@ export const getRisksForOrganization = async (req: Request, res: Response) => {
params: { organizationId } params: { organizationId }
} = await validateRequest(reqValidator.GetOrgRisksv1, req); } = await validateRequest(reqValidator.GetOrgRisksv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read, OrgPermissionActions.Read,
OrgPermissionSubjects.SecretScanning OrgPermissionSubjects.SecretScanning
@ -162,7 +169,10 @@ export const updateRisksStatus = async (req: Request, res: Response) => {
body: { status } body: { status }
} = await validateRequest(reqValidator.UpdateRiskStatusv1, req); } = await validateRequest(reqValidator.UpdateRiskStatusv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit, OrgPermissionActions.Edit,
OrgPermissionSubjects.SecretScanning OrgPermissionSubjects.SecretScanning

File diff suppressed because it is too large Load Diff

View File

@ -17,7 +17,7 @@ import { OrganizationNotFoundError } from "../../utils/errors";
import { import {
OrgPermissionActions, OrgPermissionActions,
OrgPermissionSubjects, OrgPermissionSubjects,
getUserOrgPermissions getAuthDataOrgPermissions
} from "../../ee/services/RoleService"; } from "../../ee/services/RoleService";
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { validateRequest } from "../../helpers/validation"; import { validateRequest } from "../../helpers/validation";
@ -152,7 +152,10 @@ export const createWorkspace = async (req: Request, res: Response) => {
}); });
} }
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create, OrgPermissionActions.Create,
OrgPermissionSubjects.Workspace OrgPermissionSubjects.Workspace

View File

@ -1,6 +1,11 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { Membership, MembershipOrg, Workspace } from "../../models"; import {
IdentityMembershipOrg,
Membership,
MembershipOrg,
Workspace
} from "../../models";
import { Role } from "../../ee/models"; import { Role } from "../../ee/models";
import { deleteMembershipOrg } from "../../helpers/membershipOrg"; import { deleteMembershipOrg } from "../../helpers/membershipOrg";
import { import {
@ -9,15 +14,16 @@ import {
updateSubscriptionOrgQuantity updateSubscriptionOrgQuantity
} from "../../helpers/organization"; } from "../../helpers/organization";
import { addMembershipsOrg } from "../../helpers/membershipOrg"; import { addMembershipsOrg } from "../../helpers/membershipOrg";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; import { BadRequestError, ResourceNotFoundError, UnauthorizedRequestError } from "../../utils/errors";
import { ACCEPTED, ADMIN, CUSTOM } from "../../variables"; import { ACCEPTED, ADMIN, CUSTOM, MEMBER, NO_ACCESS } from "../../variables";
import * as reqValidator from "../../validation/organization"; import * as reqValidator from "../../validation/organization";
import { validateRequest } from "../../helpers/validation"; import { validateRequest } from "../../helpers/validation";
import { import {
OrgPermissionActions, OrgPermissionActions,
OrgPermissionSubjects, OrgPermissionSubjects,
getUserOrgPermissions getAuthDataOrgPermissions
} from "../../ee/services/RoleService"; } from "../../ee/services/RoleService";
import { EELicenseService } from "../../ee/services";
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
/** /**
@ -27,11 +33,12 @@ import { ForbiddenError } from "@casl/ability";
*/ */
export const getOrganizationMemberships = async (req: Request, res: Response) => { export const getOrganizationMemberships = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Return organization memberships' #swagger.summary = 'Return organization user memberships'
#swagger.description = 'Return organization memberships' #swagger.description = 'Return organization user memberships'
#swagger.security = [{ #swagger.security = [{
"apiKeyAuth": [] "apiKeyAuth": [],
"bearerAuth": []
}] }]
#swagger.parameters['organizationId'] = { #swagger.parameters['organizationId'] = {
@ -63,7 +70,10 @@ export const getOrganizationMemberships = async (req: Request, res: Response) =>
params: { organizationId } params: { organizationId }
} = await validateRequest(reqValidator.GetOrgMembersv2, req); } = await validateRequest(reqValidator.GetOrgMembersv2, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read, OrgPermissionActions.Read,
OrgPermissionSubjects.Member OrgPermissionSubjects.Member
@ -85,11 +95,12 @@ export const getOrganizationMemberships = async (req: Request, res: Response) =>
*/ */
export const updateOrganizationMembership = async (req: Request, res: Response) => { export const updateOrganizationMembership = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Update organization membership' #swagger.summary = 'Update organization user membership'
#swagger.description = 'Update organization membership' #swagger.description = 'Update organization user membership'
#swagger.security = [{ #swagger.security = [{
"apiKeyAuth": [] "apiKeyAuth": [],
"bearerAuth": []
}] }]
#swagger.parameters['organizationId'] = { #swagger.parameters['organizationId'] = {
@ -141,17 +152,33 @@ export const updateOrganizationMembership = async (req: Request, res: Response)
params: { organizationId, membershipId }, params: { organizationId, membershipId },
body: { role } body: { role }
} = await validateRequest(reqValidator.UpdateOrgMemberv2, req); } = await validateRequest(reqValidator.UpdateOrgMemberv2, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit, OrgPermissionActions.Edit,
OrgPermissionSubjects.Member OrgPermissionSubjects.Member
); );
const isCustomRole = !["admin", "member"].includes(role); const isCustomRole = ![ADMIN, MEMBER, NO_ACCESS].includes(role);
if (isCustomRole) { if (isCustomRole) {
const orgRole = await Role.findOne({ slug: role, isOrgRole: true }); const orgRole = await Role.findOne({
slug: role,
isOrgRole: true,
organization: new Types.ObjectId(organizationId)
});
if (!orgRole) throw BadRequestError({ message: "Role not found" }); if (!orgRole) throw BadRequestError({ message: "Role not found" });
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
if (!plan.rbac) return res.status(400).send({
message:
"Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."
});
const membership = await MembershipOrg.findByIdAndUpdate(membershipId, { const membership = await MembershipOrg.findByIdAndUpdate(membershipId, {
role: CUSTOM, role: CUSTOM,
customRole: orgRole customRole: orgRole
@ -189,11 +216,12 @@ export const updateOrganizationMembership = async (req: Request, res: Response)
*/ */
export const deleteOrganizationMembership = async (req: Request, res: Response) => { export const deleteOrganizationMembership = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Delete organization membership' #swagger.summary = 'Delete organization user membership'
#swagger.description = 'Delete organization membership' #swagger.description = 'Delete organization user membership'
#swagger.security = [{ #swagger.security = [{
"apiKeyAuth": [] "apiKeyAuth": [],
"bearerAuth": []
}] }]
#swagger.parameters['organizationId'] = { #swagger.parameters['organizationId'] = {
@ -227,7 +255,18 @@ export const deleteOrganizationMembership = async (req: Request, res: Response)
const { const {
params: { organizationId, membershipId } params: { organizationId, membershipId }
} = await validateRequest(reqValidator.DeleteOrgMemberv2, req); } = await validateRequest(reqValidator.DeleteOrgMemberv2, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
const membershipOrg = await MembershipOrg.findOne({
_id: new Types.ObjectId(membershipId),
organization: new Types.ObjectId(organizationId)
});
if (!membershipOrg) throw ResourceNotFoundError();
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: membershipOrg.organization
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Delete, OrgPermissionActions.Delete,
OrgPermissionSubjects.Member OrgPermissionSubjects.Member
@ -291,7 +330,11 @@ export const getOrganizationWorkspaces = async (req: Request, res: Response) =>
params: { organizationId } params: { organizationId }
} = await validateRequest(reqValidator.GetOrgWorkspacesv2, req); } = await validateRequest(reqValidator.GetOrgWorkspacesv2, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read, OrgPermissionActions.Read,
OrgPermissionSubjects.Workspace OrgPermissionSubjects.Workspace
@ -377,3 +420,66 @@ export const deleteOrganizationById = async (req: Request, res: Response) => {
organization organization
}); });
}; };
/**
* Return list of identity memberships for organization with id [organizationId]
* @param req
* @param res
* @returns
*/
export const getOrganizationIdentityMemberships = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Return organization identity memberships'
#swagger.description = 'Return organization identity memberships'
#swagger.security = [{
"bearerAuth": []
}]
#swagger.parameters['organizationId'] = {
"description": "ID of organization",
"required": true,
"type": "string",
"in": "path"
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"identityMemberships": {
"type": "array",
"items": {
$ref: "#/components/schemas/IdentityMembershipOrg"
},
"description": "Identity memberships of organization"
}
}
}
}
}
}
*/
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgIdentityMembershipsV2, req);
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Identity
);
const identityMemberships = await IdentityMembershipOrg.find({
organization: new Types.ObjectId(organizationId)
}).populate("identity customRole");
return res.status(200).send({
identityMemberships
});
}

View File

@ -1,6 +1,15 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { Key, Membership, ServiceTokenData, Workspace } from "../../models"; import {
IIdentity,
IdentityMembership,
IdentityMembershipOrg,
Key,
Membership,
ServiceTokenData,
Workspace
} from "../../models";
import { IRole, Role } from "../../ee/models";
import { import {
pullSecrets as pull, pullSecrets as pull,
v2PushSecrets as push, v2PushSecrets as push,
@ -16,9 +25,13 @@ import * as reqValidator from "../../validation";
import { import {
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionSub, ProjectPermissionSub,
getAuthDataProjectPermissions getAuthDataProjectPermissions,
getWorkspaceRolePermissions,
isAtLeastAsPrivilegedWorkspace
} from "../../ee/services/ProjectRoleService"; } from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { BadRequestError, ForbiddenRequestError, ResourceNotFoundError } from "../../utils/errors";
import { ADMIN, CUSTOM, MEMBER, NO_ACCESS, VIEWER } from "../../variables";
interface V2PushSecret { interface V2PushSecret {
type: string; // personal or shared type: string; // personal or shared
@ -198,7 +211,7 @@ export const getWorkspaceKey = async (req: Request, res: Response) => {
receiver: req.user._id receiver: req.user._id
}).populate("sender", "+publicKey"); }).populate("sender", "+publicKey");
if (!key) throw new Error("Failed to find workspace key"); if (!key) throw new Error(`getWorkspaceKey: Failed to find workspace key [workspaceId=${workspaceId}] [receiver=${req.user._id}]`);
await EEAuditLogService.createAuditLog( await EEAuditLogService.createAuditLog(
req.authData, req.authData,
@ -236,11 +249,12 @@ export const getWorkspaceServiceTokenData = async (req: Request, res: Response)
*/ */
export const getWorkspaceMemberships = async (req: Request, res: Response) => { export const getWorkspaceMemberships = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Return project memberships' #swagger.summary = 'Return project user memberships'
#swagger.description = 'Return project memberships' #swagger.description = 'Return project user memberships'
#swagger.security = [{ #swagger.security = [{
"apiKeyAuth": [] "apiKeyAuth": [],
"bearerAuth": []
}] }]
#swagger.parameters['workspaceId'] = { #swagger.parameters['workspaceId'] = {
@ -299,11 +313,12 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => {
*/ */
export const updateWorkspaceMembership = async (req: Request, res: Response) => { export const updateWorkspaceMembership = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Update project membership' #swagger.summary = 'Update project user membership'
#swagger.description = 'Update project membership' #swagger.description = 'Update project user membership'
#swagger.security = [{ #swagger.security = [{
"apiKeyAuth": [] "apiKeyAuth": [],
"bearerAuth": []
}] }]
#swagger.parameters['workspaceId'] = { #swagger.parameters['workspaceId'] = {
@ -327,7 +342,7 @@ export const updateWorkspaceMembership = async (req: Request, res: Response) =>
"properties": { "properties": {
"role": { "role": {
"type": "string", "type": "string",
"description": "Role of membership - either admin or member", "description": "Role to update to for project membership",
} }
} }
} }
@ -389,11 +404,12 @@ export const updateWorkspaceMembership = async (req: Request, res: Response) =>
*/ */
export const deleteWorkspaceMembership = async (req: Request, res: Response) => { export const deleteWorkspaceMembership = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Delete project membership' #swagger.summary = 'Delete project user membership'
#swagger.description = 'Delete project membership' #swagger.description = 'Delete project user membership'
#swagger.security = [{ #swagger.security = [{
"apiKeyAuth": [] "apiKeyAuth": [],
"bearerAuth": []
}] }]
#swagger.parameters['workspaceId'] = { #swagger.parameters['workspaceId'] = {
@ -491,3 +507,377 @@ export const toggleAutoCapitalization = async (req: Request, res: Response) => {
workspace workspace
}); });
}; };
/**
* Add identity with id [identityId] to workspace
* with id [workspaceId]
* @param req
* @param res
*/
export const addIdentityToWorkspace = async (req: Request, res: Response) => {
const {
params: { workspaceId, identityId },
body: {
role
}
} = await validateRequest(reqValidator.AddIdentityToWorkspaceV2, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Identity
);
let identityMembership = await IdentityMembership.findOne({
identity: new Types.ObjectId(identityId),
workspace: new Types.ObjectId(workspaceId)
});
if (identityMembership) throw BadRequestError({
message: `Identity with id ${identityId} already exists in project with id ${workspaceId}`
});
const workspace = await Workspace.findById(workspaceId);
if (!workspace) throw ResourceNotFoundError();
const identityMembershipOrg = await IdentityMembershipOrg.findOne({
identity: new Types.ObjectId(identityId),
organization: workspace.organization
});
if (!identityMembershipOrg) throw ResourceNotFoundError({
message: `Failed to find identity with id ${identityId}`
});
if (!identityMembershipOrg.organization.equals(workspace.organization)) throw BadRequestError({
message: "Failed to add identity to project in another organization"
});
const rolePermission = await getWorkspaceRolePermissions(role, workspaceId);
const isAsPrivilegedAsIntendedRole = isAtLeastAsPrivilegedWorkspace(permission, rolePermission);
if (!isAsPrivilegedAsIntendedRole) throw ForbiddenRequestError({
message: "Failed to add identity to project with more privileged role"
});
let customRole;
if (role) {
const isCustomRole = ![ADMIN, MEMBER, VIEWER, NO_ACCESS].includes(role);
if (isCustomRole) {
customRole = await Role.findOne({
slug: role,
isOrgRole: false,
workspace: new Types.ObjectId(workspaceId)
});
if (!customRole) throw BadRequestError({ message: "Role not found" });
}
}
identityMembership = await new IdentityMembership({
identity: identityMembershipOrg.identity,
workspace: new Types.ObjectId(workspaceId),
role: customRole ? CUSTOM : role,
customRole
}).save();
return res.status(200).send({
identityMembership
});
}
/**
* Update role of identity with id [identityId] in workspace
* with id [workspaceId] to [role]
* @param req
* @param res
*/
export const updateIdentityWorkspaceRole = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Update project identity membership'
#swagger.description = 'Update project identity membership'
#swagger.security = [{
"bearerAuth": []
}]
#swagger.parameters['workspaceId'] = {
"description": "ID of project",
"required": true,
"type": "string"
}
#swagger.parameters['identityId'] = {
"description": "ID of identity whose membership to update in project",
"required": true,
"type": "string"
}
#swagger.requestBody = {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"role": {
"type": "string",
"description": "Role to update to for identity project membership",
}
}
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"identityMembership": {
$ref: "#/components/schemas/IdentityMembership",
"description": "Updated identity membership"
}
}
}
}
}
}
*/
const {
params: { workspaceId, identityId },
body: {
role
}
} = await validateRequest(reqValidator.UpdateIdentityWorkspaceRoleV2, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Identity
);
let identityMembership = await IdentityMembership
.findOne({
identity: new Types.ObjectId(identityId),
workspace: new Types.ObjectId(workspaceId)
})
.populate<{
identity: IIdentity,
customRole: IRole
}>("identity customRole");
if (!identityMembership) throw BadRequestError({
message: `Identity with id ${identityId} does not exist in project with id ${workspaceId}`
});
const identityRolePermission = await getWorkspaceRolePermissions(
identityMembership?.customRole?.slug ?? identityMembership.role,
identityMembership.workspace.toString()
);
const isAsPrivilegedAsIdentity = isAtLeastAsPrivilegedWorkspace(permission, identityRolePermission);
if (!isAsPrivilegedAsIdentity) throw ForbiddenRequestError({
message: "Failed to update role of more privileged identity"
});
const rolePermission = await getWorkspaceRolePermissions(role, workspaceId);
const isAsPrivilegedAsIntendedRole = isAtLeastAsPrivilegedWorkspace(permission, rolePermission);
if (!isAsPrivilegedAsIntendedRole) throw ForbiddenRequestError({
message: "Failed to update identity to a more privileged role"
});
let customRole;
if (role) {
const isCustomRole = ![ADMIN, MEMBER, VIEWER, NO_ACCESS].includes(role);
if (isCustomRole) {
customRole = await Role.findOne({
slug: role,
isOrgRole: false,
workspace: new Types.ObjectId(workspaceId)
});
if (!customRole) throw BadRequestError({ message: "Role not found" });
}
}
identityMembership = await IdentityMembership.findOneAndUpdate(
{
identity: identityMembership.identity._id,
workspace: new Types.ObjectId(workspaceId),
},
{
role: customRole ? CUSTOM : role,
customRole
},
{
new: true
}
);
return res.status(200).send({
identityMembership
});
}
/**
* Delete identity with id [identityId] from workspace
* with id [workspaceId]
* @param req
* @param res
*/
export const deleteIdentityFromWorkspace = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Delete project identity membership'
#swagger.description = 'Delete project identity membership'
#swagger.security = [{
"bearerAuth": []
}]
#swagger.parameters['workspaceId'] = {
"description": "ID of project",
"required": true,
"type": "string"
}
#swagger.parameters['identityId'] = {
"description": "ID of identity whose membership to delete in project",
"required": true,
"type": "string"
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"identityMembership": {
$ref: "#/components/schemas/IdentityMembership",
"description": "Deleted identity membership"
}
}
}
}
}
}
*/
const {
params: { workspaceId, identityId }
} = await validateRequest(reqValidator.DeleteIdentityFromWorkspaceV2, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Identity
);
const identityMembership = await IdentityMembership
.findOne({
identity: new Types.ObjectId(identityId),
workspace: new Types.ObjectId(workspaceId)
})
.populate<{
identity: IIdentity,
customRole: IRole
}>("identity customRole");
if (!identityMembership) throw ResourceNotFoundError({
message: `Identity with id ${identityId} does not exist in project with id ${workspaceId}`
});
const identityRolePermission = await getWorkspaceRolePermissions(
identityMembership?.customRole?.slug ?? identityMembership.role,
identityMembership.workspace.toString()
);
const isAsPrivilegedAsIdentity = isAtLeastAsPrivilegedWorkspace(permission, identityRolePermission);
if (!isAsPrivilegedAsIdentity) throw ForbiddenRequestError({
message: "Failed to remove more privileged identity from project"
});
await IdentityMembership.findByIdAndDelete(identityMembership._id);
return res.status(200).send({
identityMembership
});
}
/**
* Return list of identity memberships for workspace with id [workspaceId]
* @param req
* @param res
* @returns
*/
export const getWorkspaceIdentityMemberships = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Return project identity memberships'
#swagger.description = 'Return project identity memberships'
#swagger.security = [{
"bearerAuth": []
}]
#swagger.parameters['workspaceId'] = {
"description": "ID of project",
"required": true,
"type": "string",
"in": "path"
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"identityMemberships": {
"type": "array",
"items": {
$ref: "#/components/schemas/IdentityMembership"
},
"description": "Identity memberships of project"
}
}
}
}
}
}
*/
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceIdentityMembersV2, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Identity
);
const identityMemberships = await IdentityMembership.find({
workspace: new Types.ObjectId(workspaceId)
}).populate("identity customRole");
return res.status(200).send({
identityMemberships
});
}

View File

@ -94,7 +94,7 @@ const checkSecretsPermission = async ({
}); });
return { authVerifier: () => true }; return { authVerifier: () => true };
} }
case ActorType.SERVICE_V3: { case ActorType.IDENTITY: {
const { permission } = await getAuthDataProjectPermissions({ const { permission } = await getAuthDataProjectPermissions({
authData, authData,
workspaceId: new Types.ObjectId(workspaceId) workspaceId: new Types.ObjectId(workspaceId)

View File

@ -1,7 +1,7 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { validateRequest } from "../../helpers/validation"; import { validateRequest } from "../../helpers/validation";
import { Membership, Secret, ServiceTokenDataV3, User } from "../../models"; import { Membership, Secret, User } from "../../models";
import { SecretService } from "../../services"; import { SecretService } from "../../services";
import { getAuthDataProjectPermissions } from "../../ee/services/ProjectRoleService"; import { getAuthDataProjectPermissions } from "../../ee/services/ProjectRoleService";
import { UnauthorizedRequestError } from "../../utils/errors"; import { UnauthorizedRequestError } from "../../utils/errors";
@ -140,17 +140,3 @@ export const nameWorkspaceSecrets = async (req: Request, res: Response) => {
message: "Successfully named workspace secrets" message: "Successfully named workspace secrets"
}); });
}; };
export const getWorkspaceServiceTokenData = async (req: Request, res: Response) => {
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceServiceTokenDataV3, req);
const serviceTokenData = await ServiceTokenDataV3.find({
workspace: new Types.ObjectId(workspaceId)
}).populate("customRole");
return res.status(200).send({
serviceTokenData
});
}

View File

@ -0,0 +1,460 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import {
IIdentity,
Identity,
IdentityAccessToken,
IdentityMembership,
IdentityMembershipOrg,
IdentityUniversalAuth,
IdentityUniversalAuthClientSecret,
Organization
} from "../../../models";
import {
EventType,
IRole,
Role
} from "../../models";
import { validateRequest } from "../../../helpers/validation";
import * as reqValidator from "../../../validation/identities";
import {
getAuthDataOrgPermissions,
getOrgRolePermissions,
isAtLeastAsPrivilegedOrg
} from "../../services/RoleService";
import {
BadRequestError,
ForbiddenRequestError,
ResourceNotFoundError,
} from "../../../utils/errors";
import { ADMIN, CUSTOM, MEMBER, NO_ACCESS } from "../../../variables";
import {
OrgPermissionActions,
OrgPermissionSubjects
} from "../../services/RoleService";
import { EEAuditLogService } from "../../services";
import { ForbiddenError } from "@casl/ability";
/**
* Create identity
* @param req
* @param res
* @returns
*/
export const createIdentity = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Create identity'
#swagger.description = 'Create identity'
#swagger.security = [{
"bearerAuth": []
}]
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of entity to create",
"example": "development"
},
"organizationId": {
"type": "string",
"description": "ID of organization where to create identity",
"example": "dev-environment"
},
"role": {
"type": "string",
"description": "Role to assume for organization membership",
"example": "no-access"
}
},
"required": ["name", "organizationId", "role"]
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"identity": {
$ref: '#/definitions/Identity'
}
},
"description": "Details of the created identity"
}
}
}
}
*/
const {
body: {
name,
organizationId,
role
}
} = await validateRequest(reqValidator.CreateIdentityV1, req);
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create,
OrgPermissionSubjects.Identity
);
const rolePermission = await getOrgRolePermissions(role, organizationId);
const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, rolePermission);
if (!hasRequiredPrivileges) throw ForbiddenRequestError({
message: "Failed to create a more privileged identity"
});
const organization = await Organization.findById(organizationId);
if (!organization) throw BadRequestError({ message: `Organization with id ${organizationId} not found` });
const isCustomRole = ![ADMIN, MEMBER, NO_ACCESS].includes(role);
let customRole;
if (isCustomRole) {
customRole = await Role.findOne({
slug: role,
isOrgRole: true,
organization: new Types.ObjectId(organizationId)
});
if (!customRole) throw BadRequestError({ message: "Role not found" });
}
const identity = await new Identity({
name
}).save();
await new IdentityMembershipOrg({
identity: identity._id,
organization: new Types.ObjectId(organizationId),
role: isCustomRole ? CUSTOM : role,
customRole
}).save();
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_IDENTITY,
metadata: {
identityId: identity._id.toString(),
name
}
},
{
organizationId: new Types.ObjectId(organizationId)
}
);
return res.status(200).send({
identity
});
}
/**
* Update identity with id [identityId]
* @param req
* @param res
* @returns
*/
export const updateIdentity = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Update identity'
#swagger.description = 'Update identity'
#swagger.security = [{
"bearerAuth": []
}]
#swagger.parameters['identityId'] = {
"description": "ID of identity to update",
"required": true,
"type": "string",
"in": "path"
}
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of entity to update to",
"example": "development"
},
"role": {
"type": "string",
"description": "Role to update to for organization membership",
"example": "no-access"
}
}
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"identity": {
$ref: '#/definitions/Identity'
}
},
"description": "Details of the updated identity"
}
}
}
}
*/
const {
params: { identityId },
body: {
name,
role
}
} = await validateRequest(reqValidator.UpdateIdentityV1, req);
const identityMembershipOrg = await IdentityMembershipOrg
.findOne({
identity: new Types.ObjectId(identityId)
})
.populate<{
identity: IIdentity,
customRole: IRole
}>("identity customRole");
if (!identityMembershipOrg) throw ResourceNotFoundError({
message: `Failed to find identity with id ${identityId}`
});
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: identityMembershipOrg.organization
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit,
OrgPermissionSubjects.Identity
);
const identityRolePermission = await getOrgRolePermissions(
identityMembershipOrg?.customRole?.slug ?? identityMembershipOrg.role,
identityMembershipOrg.organization.toString()
);
const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, identityRolePermission);
if (!hasRequiredPrivileges) throw ForbiddenRequestError({
message: "Failed to update more privileged identity"
});
if (role) {
const rolePermission = await getOrgRolePermissions(role, identityMembershipOrg.organization.toString());
const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, rolePermission);
if (!hasRequiredPrivileges) throw ForbiddenRequestError({
message: "Failed to update identity to a more privileged role"
});
}
let customRole;
if (role) {
const isCustomRole = ![ADMIN, MEMBER, NO_ACCESS].includes(role);
if (isCustomRole) {
customRole = await Role.findOne({
slug: role,
isOrgRole: true,
organization: identityMembershipOrg.organization
});
if (!customRole) throw BadRequestError({ message: "Role not found" });
}
}
const identity = await Identity.findByIdAndUpdate(
identityId,
{
name,
},
{
new: true
}
);
if (!identity) throw BadRequestError({
message: `Failed to update identity with id ${identityId}`
});
await IdentityMembershipOrg.findOneAndUpdate(
{
identity: identity._id
},
{
role: customRole ? CUSTOM : role,
...(customRole ? {
customRole
} : {}),
...(role && !customRole ? { // non-custom role
$unset: {
customRole: 1
}
} : {})
},
{
new: true
}
);
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.UPDATE_IDENTITY,
metadata: {
identityId: identity._id.toString(),
name: identity.name,
}
},
{
organizationId: identityMembershipOrg.organization
}
);
return res.status(200).send({
identity
});
}
/**
* Delete identity with id [identityId]
* @param req
* @param res
* @returns
*/
export const deleteIdentity = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Delete identity'
#swagger.description = 'Delete identity'
#swagger.security = [{
"bearerAuth": []
}]
#swagger.parameters['identityId'] = {
"description": "ID of identity",
"required": true,
"type": "string",
"in": "path"
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"identity": {
$ref: '#/definitions/Identity'
}
},
"description": "Details of the deleted identity"
}
}
}
}
*/
const {
params: { identityId }
} = await validateRequest(reqValidator.DeleteIdentityV1, req);
const identityMembershipOrg = await IdentityMembershipOrg
.findOne({
identity: new Types.ObjectId(identityId)
})
.populate<{
identity: IIdentity,
customRole: IRole
}>("identity customRole");
if (!identityMembershipOrg) throw ResourceNotFoundError({
message: `Failed to find identity with id ${identityId}`
});
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: identityMembershipOrg.organization
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Delete,
OrgPermissionSubjects.Identity
);
const identityRolePermission = await getOrgRolePermissions(
identityMembershipOrg?.customRole?.slug ?? identityMembershipOrg.role,
identityMembershipOrg.organization.toString()
);
const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, identityRolePermission);
if (!hasRequiredPrivileges) throw ForbiddenRequestError({
message: "Failed to delete more privileged identity"
});
const identity = await Identity.findByIdAndDelete(identityMembershipOrg.identity);
if (!identity) throw ResourceNotFoundError({
message: `Identity with id ${identityId} not found`
});
await IdentityMembershipOrg.findByIdAndDelete(identityMembershipOrg._id);
await IdentityMembership.deleteMany({
identity: identityMembershipOrg.identity
});
await IdentityUniversalAuth.deleteMany({
identity: identityMembershipOrg.identity
});
await IdentityUniversalAuthClientSecret.deleteMany({
identity: identityMembershipOrg.identity
});
await IdentityAccessToken.deleteMany({
identity: identityMembershipOrg.identity
});
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.DELETE_IDENTITY,
metadata: {
identityId: identity._id.toString()
}
},
{
organizationId: identityMembershipOrg.organization
}
);
return res.status(200).send({
identity
});
}

View File

@ -1,3 +1,4 @@
import * as identitiesController from "./identitiesController";
import * as secretController from "./secretController"; import * as secretController from "./secretController";
import * as secretSnapshotController from "./secretSnapshotController"; import * as secretSnapshotController from "./secretSnapshotController";
import * as organizationsController from "./organizationsController"; import * as organizationsController from "./organizationsController";
@ -13,6 +14,7 @@ import * as secretRotationProviderController from "./secretRotationProviderContr
import * as secretRotationController from "./secretRotationController"; import * as secretRotationController from "./secretRotationController";
export { export {
identitiesController,
secretController, secretController,
secretSnapshotController, secretSnapshotController,
organizationsController, organizationsController,

View File

@ -8,7 +8,7 @@ import * as reqValidator from "../../../validation/organization";
import { import {
OrgPermissionActions, OrgPermissionActions,
OrgPermissionSubjects, OrgPermissionSubjects,
getUserOrgPermissions getAuthDataOrgPermissions,
} from "../../services/RoleService"; } from "../../services/RoleService";
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { Organization } from "../../../models"; import { Organization } from "../../../models";
@ -20,7 +20,10 @@ export const getOrganizationPlansTable = async (req: Request, res: Response) =>
params: { organizationId } params: { organizationId }
} = await validateRequest(reqValidator.GetOrgPlansTablev1, req); } = await validateRequest(reqValidator.GetOrgPlansTablev1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read, OrgPermissionActions.Read,
OrgPermissionSubjects.Billing OrgPermissionSubjects.Billing
@ -42,7 +45,10 @@ export const getOrganizationPlan = async (req: Request, res: Response) => {
params: { organizationId } params: { organizationId }
} = await validateRequest(reqValidator.GetOrgPlanv1, req); } = await validateRequest(reqValidator.GetOrgPlanv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read, OrgPermissionActions.Read,
OrgPermissionSubjects.Billing OrgPermissionSubjects.Billing
@ -70,7 +76,10 @@ export const startOrganizationTrial = async (req: Request, res: Response) => {
body: { success_url } body: { success_url }
} = await validateRequest(reqValidator.StartOrgTrailv1, req); } = await validateRequest(reqValidator.StartOrgTrailv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create, OrgPermissionActions.Create,
OrgPermissionSubjects.Billing OrgPermissionSubjects.Billing
@ -116,7 +125,10 @@ export const getOrganizationPlanBillingInfo = async (req: Request, res: Response
params: { organizationId } params: { organizationId }
} = await validateRequest(reqValidator.GetOrgPlanBillingInfov1, req); } = await validateRequest(reqValidator.GetOrgPlanBillingInfov1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read, OrgPermissionActions.Read,
OrgPermissionSubjects.Billing OrgPermissionSubjects.Billing
@ -149,7 +161,10 @@ export const getOrganizationPlanTable = async (req: Request, res: Response) => {
params: { organizationId } params: { organizationId }
} = await validateRequest(reqValidator.GetOrgPlanTablev1, req); } = await validateRequest(reqValidator.GetOrgPlanTablev1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read, OrgPermissionActions.Read,
OrgPermissionSubjects.Billing OrgPermissionSubjects.Billing
@ -176,7 +191,10 @@ export const getOrganizationBillingDetails = async (req: Request, res: Response)
params: { organizationId } params: { organizationId }
} = await validateRequest(reqValidator.GetOrgBillingDetailsv1, req); } = await validateRequest(reqValidator.GetOrgBillingDetailsv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read, OrgPermissionActions.Read,
OrgPermissionSubjects.Billing OrgPermissionSubjects.Billing
@ -204,7 +222,10 @@ export const updateOrganizationBillingDetails = async (req: Request, res: Respon
body: { name, email } body: { name, email }
} = await validateRequest(reqValidator.UpdateOrgBillingDetailsv1, req); } = await validateRequest(reqValidator.UpdateOrgBillingDetailsv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit, OrgPermissionActions.Edit,
OrgPermissionSubjects.Billing OrgPermissionSubjects.Billing
@ -238,7 +259,10 @@ export const getOrganizationPmtMethods = async (req: Request, res: Response) =>
params: { organizationId } params: { organizationId }
} = await validateRequest(reqValidator.GetOrgPmtMethodsv1, req); } = await validateRequest(reqValidator.GetOrgPmtMethodsv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read, OrgPermissionActions.Read,
OrgPermissionSubjects.Billing OrgPermissionSubjects.Billing
@ -271,7 +295,10 @@ export const addOrganizationPmtMethod = async (req: Request, res: Response) => {
body: { success_url, cancel_url } body: { success_url, cancel_url }
} = await validateRequest(reqValidator.CreateOrgPmtMethodv1, req); } = await validateRequest(reqValidator.CreateOrgPmtMethodv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create, OrgPermissionActions.Create,
OrgPermissionSubjects.Billing OrgPermissionSubjects.Billing
@ -312,7 +339,10 @@ export const deleteOrganizationPmtMethod = async (req: Request, res: Response) =
params: { organizationId, pmtMethodId } params: { organizationId, pmtMethodId }
} = await validateRequest(reqValidator.DelOrgPmtMethodv1, req); } = await validateRequest(reqValidator.DelOrgPmtMethodv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Delete, OrgPermissionActions.Delete,
OrgPermissionSubjects.Billing OrgPermissionSubjects.Billing
@ -342,7 +372,10 @@ export const getOrganizationTaxIds = async (req: Request, res: Response) => {
params: { organizationId } params: { organizationId }
} = await validateRequest(reqValidator.GetOrgTaxIdsv1, req); } = await validateRequest(reqValidator.GetOrgTaxIdsv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read, OrgPermissionActions.Read,
OrgPermissionSubjects.Billing OrgPermissionSubjects.Billing
@ -375,7 +408,10 @@ export const addOrganizationTaxId = async (req: Request, res: Response) => {
body: { type, value } body: { type, value }
} = await validateRequest(reqValidator.CreateOrgTaxId, req); } = await validateRequest(reqValidator.CreateOrgTaxId, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create, OrgPermissionActions.Create,
OrgPermissionSubjects.Billing OrgPermissionSubjects.Billing
@ -412,7 +448,10 @@ export const deleteOrganizationTaxId = async (req: Request, res: Response) => {
params: { organizationId, taxId } params: { organizationId, taxId }
} = await validateRequest(reqValidator.DelOrgTaxIdv1, req); } = await validateRequest(reqValidator.DelOrgTaxIdv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Delete, OrgPermissionActions.Delete,
OrgPermissionSubjects.Billing OrgPermissionSubjects.Billing
@ -445,7 +484,10 @@ export const getOrganizationInvoices = async (req: Request, res: Response) => {
params: { organizationId } params: { organizationId }
} = await validateRequest(reqValidator.GetOrgInvoicesv1, req); } = await validateRequest(reqValidator.GetOrgInvoicesv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read, OrgPermissionActions.Read,
OrgPermissionSubjects.Billing OrgPermissionSubjects.Billing
@ -480,7 +522,10 @@ export const getOrganizationLicenses = async (req: Request, res: Response) => {
params: { organizationId } params: { organizationId }
} = await validateRequest(reqValidator.GetOrgLicencesv1, req); } = await validateRequest(reqValidator.GetOrgLicencesv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read, OrgPermissionActions.Read,
OrgPermissionSubjects.Billing OrgPermissionSubjects.Billing

View File

@ -15,14 +15,17 @@ import {
adminProjectPermissions, adminProjectPermissions,
getAuthDataProjectPermissions, getAuthDataProjectPermissions,
memberProjectPermissions, memberProjectPermissions,
noAccessProjectPermissions,
viewerProjectPermission viewerProjectPermission
} from "../../services/ProjectRoleService"; } from "../../services/ProjectRoleService";
import { import {
OrgPermissionActions, OrgPermissionActions,
OrgPermissionSubjects, OrgPermissionSubjects,
adminPermissions, adminPermissions,
getAuthDataOrgPermissions,
getUserOrgPermissions, getUserOrgPermissions,
memberPermissions memberPermissions,
noAccessPermissions
} from "../../services/RoleService"; } from "../../services/RoleService";
import { BadRequestError } from "../../../utils/errors"; import { BadRequestError } from "../../../utils/errors";
import { Role } from "../../models"; import { Role } from "../../models";
@ -36,7 +39,11 @@ export const createRole = async (req: Request, res: Response) => {
const isOrgRole = !workspaceId; // if workspaceid is provided then its a workspace rule const isOrgRole = !workspaceId; // if workspaceid is provided then its a workspace rule
if (isOrgRole) { if (isOrgRole) {
const { permission } = await getUserOrgPermissions(req.user.id, orgId); const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(orgId)
});
if (permission.cannot(OrgPermissionActions.Create, OrgPermissionSubjects.Role)) { if (permission.cannot(OrgPermissionActions.Create, OrgPermissionSubjects.Role)) {
throw BadRequestError({ message: "user doesn't have the permission." }); throw BadRequestError({ message: "user doesn't have the permission." });
} }
@ -82,7 +89,10 @@ export const updateRole = async (req: Request, res: Response) => {
const isOrgRole = !workspaceId; // if workspaceid is provided then its a workspace rule const isOrgRole = !workspaceId; // if workspaceid is provided then its a workspace rule
if (isOrgRole) { if (isOrgRole) {
const { permission } = await getUserOrgPermissions(req.user.id, orgId); const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(orgId)
});
if (permission.cannot(OrgPermissionActions.Edit, OrgPermissionSubjects.Role)) { if (permission.cannot(OrgPermissionActions.Edit, OrgPermissionSubjects.Role)) {
throw BadRequestError({ message: "User doesn't have the org permission." }); throw BadRequestError({ message: "User doesn't have the org permission." });
} }
@ -138,7 +148,10 @@ export const deleteRole = async (req: Request, res: Response) => {
const isOrgRole = !role.workspace; const isOrgRole = !role.workspace;
if (isOrgRole) { if (isOrgRole) {
const { permission } = await getUserOrgPermissions(req.user.id, role.organization.toString()); const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: role.organization
});
if (permission.cannot(OrgPermissionActions.Delete, OrgPermissionSubjects.Role)) { if (permission.cannot(OrgPermissionActions.Delete, OrgPermissionSubjects.Role)) {
throw BadRequestError({ message: "User doesn't have the org permission." }); throw BadRequestError({ message: "User doesn't have the org permission." });
} }
@ -170,7 +183,10 @@ export const getRoles = async (req: Request, res: Response) => {
const isOrgRole = !workspaceId; const isOrgRole = !workspaceId;
if (isOrgRole) { if (isOrgRole) {
const { permission } = await getUserOrgPermissions(req.user.id, orgId); const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(orgId)
});
if (permission.cannot(OrgPermissionActions.Read, OrgPermissionSubjects.Role)) { if (permission.cannot(OrgPermissionActions.Read, OrgPermissionSubjects.Role)) {
throw BadRequestError({ message: "User doesn't have the org permission." }); throw BadRequestError({ message: "User doesn't have the org permission." });
} }
@ -195,6 +211,13 @@ export const getRoles = async (req: Request, res: Response) => {
description: "Complete administration access over the organization", description: "Complete administration access over the organization",
permissions: isOrgRole ? adminPermissions.rules : adminProjectPermissions.rules permissions: isOrgRole ? adminPermissions.rules : adminProjectPermissions.rules
}, },
{
_id: "no-access",
name: "No Access",
slug: "no-access",
description: "No access to any resources in the organization",
permissions: isOrgRole ? noAccessPermissions.rules : noAccessProjectPermissions.rules
},
{ {
_id: "member", _id: "member",
name: isOrgRole ? "Member" : "Developer", name: isOrgRole ? "Member" : "Developer",

View File

@ -13,7 +13,7 @@ import { validateRequest } from "../../../helpers/validation";
import { import {
OrgPermissionActions, OrgPermissionActions,
OrgPermissionSubjects, OrgPermissionSubjects,
getUserOrgPermissions getAuthDataOrgPermissions
} from "../../services/RoleService"; } from "../../services/RoleService";
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
@ -47,7 +47,10 @@ export const getSSOConfig = async (req: Request, res: Response) => {
query: { organizationId } query: { organizationId }
} = await validateRequest(reqValidator.GetSsoConfigv1, req); } = await validateRequest(reqValidator.GetSsoConfigv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read, OrgPermissionActions.Read,
OrgPermissionSubjects.Sso OrgPermissionSubjects.Sso
@ -71,7 +74,10 @@ export const updateSSOConfig = async (req: Request, res: Response) => {
body: { organizationId, authProvider, isActive, entryPoint, issuer, cert } body: { organizationId, authProvider, isActive, entryPoint, issuer, cert }
} = await validateRequest(reqValidator.UpdateSsoConfigv1, req); } = await validateRequest(reqValidator.UpdateSsoConfigv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit, OrgPermissionActions.Edit,
OrgPermissionSubjects.Sso OrgPermissionSubjects.Sso
@ -206,7 +212,10 @@ export const createSSOConfig = async (req: Request, res: Response) => {
body: { organizationId, authProvider, isActive, entryPoint, issuer, cert } body: { organizationId, authProvider, isActive, entryPoint, issuer, cert }
} = await validateRequest(reqValidator.CreateSsoConfigv1, req); } = await validateRequest(reqValidator.CreateSsoConfigv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create, OrgPermissionActions.Create,
OrgPermissionSubjects.Sso OrgPermissionSubjects.Sso

View File

@ -2,10 +2,11 @@ import { Request, Response } from "express";
import { PipelineStage, Types } from "mongoose"; import { PipelineStage, Types } from "mongoose";
import { import {
Folder, Folder,
Identity,
IdentityMembership,
Membership, Membership,
Secret, Secret,
ServiceTokenData, ServiceTokenData,
ServiceTokenDataV3,
TFolderSchema, TFolderSchema,
User, User,
Workspace Workspace
@ -17,10 +18,10 @@ import {
FolderVersion, FolderVersion,
IPType, IPType,
ISecretVersion, ISecretVersion,
IdentityActor,
SecretSnapshot, SecretSnapshot,
SecretVersion, SecretVersion,
ServiceActor, ServiceActor,
ServiceActorV3,
TFolderRootVersionSchema, TFolderRootVersionSchema,
TrustedIP, TrustedIP,
UserActor UserActor
@ -61,15 +62,30 @@ export const getWorkspaceSecretSnapshots = async (req: Request, res: Response) =
#swagger.description = 'Return project secret snapshots ids' #swagger.description = 'Return project secret snapshots ids'
#swagger.security = [{ #swagger.security = [{
"apiKeyAuth": [] "apiKeyAuth": [],
"bearerAuth": []
}] }]
#swagger.parameters['workspaceId'] = { #swagger.parameters['workspaceId'] = {
"description": "ID of project", "description": "ID of project where to get secret snapshots for",
"required": true, "required": true,
"type": "string" "type": "string"
} }
#swagger.parameters['environment'] = {
"description": "Slug of environment where to get secret snapshots for",
"required": true,
"type": "string",
"in": "query"
}
#swagger.parameters['directory'] = {
"description": "Path where to get secret snapshots for like / or /foo/bar. Default is /",
"required": false,
"type": "string",
"in": "query"
}
#swagger.parameters['offset'] = { #swagger.parameters['offset'] = {
"description": "Number of secret snapshots to skip", "description": "Number of secret snapshots to skip",
"required": false, "required": false,
@ -194,11 +210,12 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
#swagger.description = 'Roll back project secrets to those captured in a secret snapshot version.' #swagger.description = 'Roll back project secrets to those captured in a secret snapshot version.'
#swagger.security = [{ #swagger.security = [{
"apiKeyAuth": [] "apiKeyAuth": [],
"bearerAuth": []
}] }]
#swagger.parameters['workspaceId'] = { #swagger.parameters['workspaceId'] = {
"description": "ID of project", "description": "ID of project where to roll back",
"required": true, "required": true,
"type": "string" "type": "string"
} }
@ -210,6 +227,14 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
"schema": { "schema": {
"type": "object", "type": "object",
"properties": { "properties": {
"environment": {
"type": "string",
"description": "Slug of environment where to roll back"
},
"directory": {
"type": "string",
"description": "Path where to roll back for like / or /foo/bar. Default is /"
},
"version": { "version": {
"type": "integer", "type": "integer",
"description": "Version of secret snapshot to roll back to", "description": "Version of secret snapshot to roll back to",
@ -669,6 +694,21 @@ export const getWorkspaceAuditLogs = async (req: Request, res: Response) => {
ProjectPermissionSub.AuditLogs ProjectPermissionSub.AuditLogs
); );
let actorMetadataQuery = "";
if (actor) {
switch (actor?.split("-", 2)[0]) {
case ActorType.USER:
actorMetadataQuery = "actor.metadata.userId";
break;
case ActorType.SERVICE:
actorMetadataQuery = "actor.metadata.serviceId";
break;
case ActorType.IDENTITY:
actorMetadataQuery = "actor.metadata.identityId";
break;
}
}
const query = { const query = {
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
...(eventType ...(eventType
@ -684,12 +724,8 @@ export const getWorkspaceAuditLogs = async (req: Request, res: Response) => {
...(actor ...(actor
? { ? {
"actor.type": actor.substring(0, actor.lastIndexOf("-")), "actor.type": actor.substring(0, actor.lastIndexOf("-")),
...(actor.split("-", 2)[0] === ActorType.USER ...({
? { [actorMetadataQuery]: actor.substring(actor.lastIndexOf("-") + 1)
"actor.metadata.userId": actor.substring(actor.lastIndexOf("-") + 1)
}
: {
"actor.metadata.serviceId": actor.substring(actor.lastIndexOf("-") + 1)
}) })
} }
: {}), : {}),
@ -702,7 +738,9 @@ export const getWorkspaceAuditLogs = async (req: Request, res: Response) => {
} }
: {}) : {})
}; };
const auditLogs = await AuditLog.find(query).sort({ createdAt: -1 }).skip(offset).limit(limit); const auditLogs = await AuditLog.find(query).sort({ createdAt: -1 }).skip(offset).limit(limit);
return res.status(200).send({ return res.status(200).send({
auditLogs auditLogs
}); });
@ -731,6 +769,7 @@ export const getWorkspaceAuditLogActorFilterOpts = async (req: Request, res: Res
const userIds = await Membership.distinct("user", { const userIds = await Membership.distinct("user", {
workspace: new Types.ObjectId(workspaceId) workspace: new Types.ObjectId(workspaceId)
}); });
const userActors: UserActor[] = ( const userActors: UserActor[] = (
await User.find({ await User.find({
_id: { _id: {
@ -757,19 +796,25 @@ export const getWorkspaceAuditLogActorFilterOpts = async (req: Request, res: Res
} }
})); }));
const serviceV3Actors: ServiceActorV3[] = ( const identityIds = await IdentityMembership.distinct("identity", {
await ServiceTokenDataV3.find({
workspace: new Types.ObjectId(workspaceId) workspace: new Types.ObjectId(workspaceId)
});
const identityActors: IdentityActor[] = (
await Identity.find({
_id: {
$in: identityIds
}
}) })
).map((serviceTokenData) => ({ ).map((identity) => ({
type: ActorType.SERVICE_V3, type: ActorType.IDENTITY,
metadata: { metadata: {
serviceId: serviceTokenData._id.toString(), identityId: identity._id.toString(),
name: serviceTokenData.name name: identity.name
} }
})); }));
const actors = [...userActors, ...serviceActors, ...serviceV3Actors]; const actors = [...userActors, ...serviceActors, ...identityActors];
return res.status(200).send({ return res.status(200).send({
actors actors

View File

@ -1,7 +1,5 @@
import * as serviceTokenDataController from "./serviceTokenDataController";
import * as apiKeyDataController from "./apiKeyDataController"; import * as apiKeyDataController from "./apiKeyDataController";
export { export {
serviceTokenDataController,
apiKeyDataController apiKeyDataController
} }

View File

@ -1,469 +0,0 @@
import jwt from "jsonwebtoken";
import { Request, Response } from "express";
import { Types } from "mongoose";
import {
IServiceTokenDataV3,
IUser,
ServiceTokenDataV3,
ServiceTokenDataV3Key,
Workspace
} from "../../../models";
import { IServiceTokenV3TrustedIp } from "../../../models/serviceTokenDataV3";
import {
ActorType,
EventType,
Role
} from "../../models";
import { validateRequest } from "../../../helpers/validation";
import * as reqValidator from "../../../validation/serviceTokenDataV3";
import { createToken } from "../../../helpers/auth";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getAuthDataProjectPermissions
} from "../../services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
import { BadRequestError, ResourceNotFoundError, UnauthorizedRequestError } from "../../../utils/errors";
import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip";
import { EEAuditLogService, EELicenseService } from "../../services";
import { getAuthSecret } from "../../../config";
import { ADMIN, AuthTokenType, CUSTOM, MEMBER, VIEWER } from "../../../variables";
/**
* Return project key for service token V3
* @param req
* @param res
*/
export const getServiceTokenDataKey = async (req: Request, res: Response) => {
const key = await ServiceTokenDataV3Key.findOne({
serviceTokenData: (req.authData.authPayload as IServiceTokenDataV3)._id
}).populate<{ sender: IUser }>("sender", "publicKey");
if (!key) throw ResourceNotFoundError({
message: "Failed to find project key for service token"
});
const { _id, workspace, encryptedKey, nonce, sender: { publicKey } } = key;
return res.status(200).send({
key: {
_id,
workspace,
encryptedKey,
publicKey,
nonce
}
});
}
/**
* Return access and refresh token as per refresh operation
* @param req
* @param res
*/
export const refreshToken = async (req: Request, res: Response) => {
const {
body: {
refresh_token
}
} = await validateRequest(reqValidator.RefreshTokenV3, req);
const decodedToken = <jwt.ServiceRefreshTokenJwtPayload>(
jwt.verify(refresh_token, await getAuthSecret())
);
if (decodedToken.authTokenType !== AuthTokenType.SERVICE_REFRESH_TOKEN) throw UnauthorizedRequestError();
let serviceTokenData = await ServiceTokenDataV3.findOne({
_id: new Types.ObjectId(decodedToken.serviceTokenDataId),
isActive: true
});
if (!serviceTokenData) throw UnauthorizedRequestError();
if (decodedToken.tokenVersion !== serviceTokenData.tokenVersion) {
// raise alarm
throw UnauthorizedRequestError();
}
const response: {
refresh_token?: string;
access_token: string;
expires_in: number;
token_type: string;
} = {
refresh_token,
access_token: "",
expires_in: 0,
token_type: "Bearer"
};
if (serviceTokenData.isRefreshTokenRotationEnabled) {
serviceTokenData = await ServiceTokenDataV3.findByIdAndUpdate(
serviceTokenData._id,
{
$inc: {
tokenVersion: 1
}
},
{
new: true
}
);
if (!serviceTokenData) throw BadRequestError();
response.refresh_token = createToken({
payload: {
serviceTokenDataId: serviceTokenData._id.toString(),
authTokenType: AuthTokenType.SERVICE_REFRESH_TOKEN,
tokenVersion: serviceTokenData.tokenVersion
},
secret: await getAuthSecret()
});
}
response.access_token = createToken({
payload: {
serviceTokenDataId: serviceTokenData._id.toString(),
authTokenType: AuthTokenType.SERVICE_ACCESS_TOKEN,
tokenVersion: serviceTokenData.tokenVersion
},
expiresIn: serviceTokenData.accessTokenTTL,
secret: await getAuthSecret()
});
response.expires_in = serviceTokenData.accessTokenTTL;
await ServiceTokenDataV3.findByIdAndUpdate(
serviceTokenData._id,
{
refreshTokenLastUsed: new Date(),
$inc: { refreshTokenUsageCount: 1 }
},
{
new: true
}
);
return res.status(200).send(response);
}
/**
* Create service token data V3
* @param req
* @param res
* @returns
*/
export const createServiceTokenData = async (req: Request, res: Response) => {
const {
body: {
name,
workspaceId,
publicKey,
role,
trustedIps,
expiresIn,
accessTokenTTL,
isRefreshTokenRotationEnabled,
encryptedKey, // for ServiceTokenDataV3Key
nonce, // for ServiceTokenDataV3Key
}
} = await validateRequest(reqValidator.CreateServiceTokenV3, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.ServiceTokens
);
const workspace = await Workspace.findById(workspaceId);
if (!workspace) throw BadRequestError({ message: "Workspace not found" });
const isCustomRole = ![ADMIN, MEMBER, VIEWER].includes(role);
let customRole;
if (isCustomRole) {
customRole = await Role.findOne({
slug: role,
isOrgRole: false,
workspace: workspace._id
});
if (!customRole) throw BadRequestError({ message: "Role not found" });
}
const plan = await EELicenseService.getPlan(workspace.organization);
// validate trusted ips
const reformattedTrustedIps = trustedIps.map((trustedIp) => {
if (!plan.ipAllowlisting && trustedIp.ipAddress !== "0.0.0.0/0") return res.status(400).send({
message: "Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range."
});
const isValidIPOrCidr = isValidIpOrCidr(trustedIp.ipAddress);
if (!isValidIPOrCidr) return res.status(400).send({
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
});
return extractIPDetails(trustedIp.ipAddress);
});
let expiresAt;
if (expiresIn) {
expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
}
let user;
if (req.authData.actor.type === ActorType.USER) {
user = req.authData.authPayload._id;
}
const isActive = true;
const serviceTokenData = await new ServiceTokenDataV3({
name,
user,
workspace: new Types.ObjectId(workspaceId),
publicKey,
refreshTokenUsageCount: 0,
accessTokenUsageCount: 0,
tokenVersion: 1,
trustedIps: reformattedTrustedIps,
role: isCustomRole ? CUSTOM : role,
customRole,
isActive,
expiresAt,
accessTokenTTL,
isRefreshTokenRotationEnabled
}).save();
await new ServiceTokenDataV3Key({
encryptedKey,
nonce,
sender: req.user._id,
serviceTokenData: serviceTokenData._id,
workspace: new Types.ObjectId(workspaceId)
}).save();
const refreshToken = createToken({
payload: {
serviceTokenDataId: serviceTokenData._id.toString(),
authTokenType: AuthTokenType.SERVICE_REFRESH_TOKEN,
tokenVersion: serviceTokenData.tokenVersion
},
secret: await getAuthSecret()
});
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_SERVICE_TOKEN_V3, // TODO: update
metadata: {
name,
isActive,
role,
trustedIps: reformattedTrustedIps as Array<IServiceTokenV3TrustedIp>,
expiresAt
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
return res.status(200).send({
serviceTokenData,
refreshToken
});
}
/**
* Update service token V3 data with id [serviceTokenDataId]
* @param req
* @param res
* @returns
*/
export const updateServiceTokenData = async (req: Request, res: Response) => {
const {
params: { serviceTokenDataId },
body: {
name,
isActive,
role,
trustedIps,
expiresIn,
accessTokenTTL,
isRefreshTokenRotationEnabled
}
} = await validateRequest(reqValidator.UpdateServiceTokenV3, req);
let serviceTokenData = await ServiceTokenDataV3.findById(serviceTokenDataId);
if (!serviceTokenData) throw ResourceNotFoundError({
message: "Service token not found"
});
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: serviceTokenData.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.ServiceTokens
);
const workspace = await Workspace.findById(serviceTokenData.workspace);
if (!workspace) throw BadRequestError({ message: "Workspace not found" });
let customRole;
if (role) {
const isCustomRole = ![ADMIN, MEMBER, VIEWER].includes(role);
if (isCustomRole) {
customRole = await Role.findOne({
slug: role,
isOrgRole: false,
workspace: workspace._id
});
if (!customRole) throw BadRequestError({ message: "Role not found" });
}
}
const plan = await EELicenseService.getPlan(workspace.organization);
// validate trusted ips
let reformattedTrustedIps;
if (trustedIps) {
reformattedTrustedIps = trustedIps.map((trustedIp) => {
if (!plan.ipAllowlisting && trustedIp.ipAddress !== "0.0.0.0/0") return res.status(400).send({
message: "Failed to update IP access range to service token due to plan restriction. Upgrade plan to update IP access range."
});
const isValidIPOrCidr = isValidIpOrCidr(trustedIp.ipAddress);
if (!isValidIPOrCidr) return res.status(400).send({
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
});
return extractIPDetails(trustedIp.ipAddress);
});
}
let expiresAt;
if (expiresIn) {
expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
}
serviceTokenData = await ServiceTokenDataV3.findByIdAndUpdate(
serviceTokenDataId,
{
name,
isActive,
role: customRole ? CUSTOM : role,
...(customRole ? {
customRole
} : {}),
...(role && !customRole ? { // non-custom role
$unset: {
customRole: 1
}
} : {}),
trustedIps: reformattedTrustedIps,
expiresAt,
accessTokenTTL,
isRefreshTokenRotationEnabled
},
{
new: true
}
);
if (!serviceTokenData) throw BadRequestError({
message: "Failed to update service token"
});
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.UPDATE_SERVICE_TOKEN_V3,
metadata: {
name: serviceTokenData.name,
isActive,
role,
trustedIps: reformattedTrustedIps as Array<IServiceTokenV3TrustedIp>,
expiresAt
}
},
{
workspaceId: serviceTokenData.workspace
}
);
return res.status(200).send({
serviceTokenData
});
}
/**
* Delete service token data with id [serviceTokenDataId]
* @param req
* @param res
* @returns
*/
export const deleteServiceTokenData = async (req: Request, res: Response) => {
const {
params: { serviceTokenDataId }
} = await validateRequest(reqValidator.DeleteServiceTokenV3, req);
let serviceTokenData = await ServiceTokenDataV3.findById(serviceTokenDataId);
if (!serviceTokenData) throw ResourceNotFoundError({
message: "Service token not found"
});
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: serviceTokenData.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.ServiceTokens
);
serviceTokenData = await ServiceTokenDataV3.findByIdAndDelete(serviceTokenDataId);
if (!serviceTokenData) throw BadRequestError({
message: "Failed to delete service token"
});
await ServiceTokenDataV3Key.findOneAndDelete({
serviceTokenData: serviceTokenData._id
});
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.DELETE_SERVICE_TOKEN_V3,
metadata: {
name: serviceTokenData.name,
isActive: serviceTokenData.isActive,
role: serviceTokenData.role,
trustedIps: serviceTokenData.trustedIps as Array<IServiceTokenV3TrustedIp>,
expiresAt: serviceTokenData.expiresAt
}
},
{
workspaceId: serviceTokenData.workspace
}
);
return res.status(200).send({
serviceTokenData
});
}

View File

@ -10,7 +10,7 @@ export interface IAuditLog {
event: Event; event: Event;
userAgent: string; userAgent: string;
userAgentType: UserAgentType; userAgentType: UserAgentType;
expiresAt: Date; expiresAt?: Date;
} }
const auditLogSchema = new Schema<IAuditLog>( const auditLogSchema = new Schema<IAuditLog>(

View File

@ -1,8 +1,7 @@
export enum ActorType { export enum ActorType { // would extend to AWS, Azure, ...
USER = "user", USER = "user", // userIdentity
SERVICE = "service", SERVICE = "service",
SERVICE_V3 = "service-v3", IDENTITY = "identity"
// Machine = "machine"
} }
export enum UserAgentType { export enum UserAgentType {
@ -32,9 +31,16 @@ export enum EventType {
DELETE_TRUSTED_IP = "delete-trusted-ip", DELETE_TRUSTED_IP = "delete-trusted-ip",
CREATE_SERVICE_TOKEN = "create-service-token", // v2 CREATE_SERVICE_TOKEN = "create-service-token", // v2
DELETE_SERVICE_TOKEN = "delete-service-token", // v2 DELETE_SERVICE_TOKEN = "delete-service-token", // v2
CREATE_SERVICE_TOKEN_V3 = "create-service-token-v3", // v3 CREATE_IDENTITY = "create-identity",
UPDATE_SERVICE_TOKEN_V3 = "update-service-token-v3", // v3 UPDATE_IDENTITY = "update-identity",
DELETE_SERVICE_TOKEN_V3 = "delete-service-token-v3", // v3 DELETE_IDENTITY = "delete-identity",
LOGIN_IDENTITY_UNIVERSAL_AUTH = "login-identity-universal-auth",
ADD_IDENTITY_UNIVERSAL_AUTH = "add-identity-universal-auth",
UPDATE_IDENTITY_UNIVERSAL_AUTH = "update-identity-universal-auth",
GET_IDENTITY_UNIVERSAL_AUTH = "get-identity-universal-auth",
CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret",
REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret",
GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-identity-universal-auth-client-secret",
CREATE_ENVIRONMENT = "create-environment", CREATE_ENVIRONMENT = "create-environment",
UPDATE_ENVIRONMENT = "update-environment", UPDATE_ENVIRONMENT = "update-environment",
DELETE_ENVIRONMENT = "delete-environment", DELETE_ENVIRONMENT = "delete-environment",

View File

@ -1,5 +1,5 @@
import { ActorType, EventType } from "./enums"; import { ActorType, EventType } from "./enums";
import { IServiceTokenV3TrustedIp } from "../../../models/serviceTokenDataV3"; import { IIdentityTrustedIp } from "../../../models";
interface UserActorMetadata { interface UserActorMetadata {
userId: string; userId: string;
@ -11,6 +11,11 @@ interface ServiceActorMetadata {
name: string; name: string;
} }
interface IdentityActorMetadata {
identityId: string;
name: string;
}
export interface UserActor { export interface UserActor {
type: ActorType.USER; type: ActorType.USER;
metadata: UserActorMetadata; metadata: UserActorMetadata;
@ -21,16 +26,12 @@ export interface ServiceActor {
metadata: ServiceActorMetadata; metadata: ServiceActorMetadata;
} }
export interface ServiceActorV3 { export interface IdentityActor {
type: ActorType.SERVICE_V3; type: ActorType.IDENTITY;
metadata: ServiceActorMetadata; metadata: IdentityActorMetadata;
} }
// export interface MachineActor { export type Actor = UserActor | ServiceActor | IdentityActor;
// type: ActorType.Machine;
// }
export type Actor = UserActor | ServiceActor | ServiceActorV3;
interface GetSecretsEvent { interface GetSecretsEvent {
type: EventType.GET_SECRETS; type: EventType.GET_SECRETS;
@ -220,36 +221,91 @@ interface DeleteServiceTokenEvent {
}; };
} }
interface CreateServiceTokenV3Event { interface CreateIdentityEvent { // note: currently not logging org-role
type: EventType.CREATE_SERVICE_TOKEN_V3; type: EventType.CREATE_IDENTITY;
metadata: { metadata: {
identityId: string;
name: string; name: string;
isActive: boolean;
role: string;
trustedIps: Array<IServiceTokenV3TrustedIp>;
expiresAt?: Date;
}; };
} }
interface UpdateServiceTokenV3Event { interface UpdateIdentityEvent {
type: EventType.UPDATE_SERVICE_TOKEN_V3; type: EventType.UPDATE_IDENTITY;
metadata: { metadata: {
identityId: string;
name?: string; name?: string;
isActive?: boolean;
role?: string;
trustedIps?: Array<IServiceTokenV3TrustedIp>;
expiresAt?: Date;
}; };
} }
interface DeleteServiceTokenV3Event { interface DeleteIdentityEvent {
type: EventType.DELETE_SERVICE_TOKEN_V3; type: EventType.DELETE_IDENTITY;
metadata: { metadata: {
name: string; identityId: string;
isActive: boolean; };
role: string; }
expiresAt?: Date;
trustedIps: Array<IServiceTokenV3TrustedIp>; interface LoginIdentityUniversalAuthEvent {
type: EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH ;
metadata: {
identityId: string;
identityUniversalAuthId: string;
clientSecretId: string;
identityAccessTokenId: string;
};
}
interface AddIdentityUniversalAuthEvent {
type: EventType.ADD_IDENTITY_UNIVERSAL_AUTH;
metadata: {
identityId: string;
clientSecretTrustedIps: Array<IIdentityTrustedIp>;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: Array<IIdentityTrustedIp>;
};
}
interface UpdateIdentityUniversalAuthEvent {
type: EventType.UPDATE_IDENTITY_UNIVERSAL_AUTH;
metadata: {
identityId: string;
clientSecretTrustedIps?: Array<IIdentityTrustedIp>;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;
accessTokenTrustedIps?: Array<IIdentityTrustedIp>;
};
}
interface GetIdentityUniversalAuthEvent {
type: EventType.GET_IDENTITY_UNIVERSAL_AUTH;
metadata: {
identityId: string;
};
}
interface CreateIdentityUniversalAuthClientSecretEvent {
type: EventType.CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET ;
metadata: {
identityId: string;
clientSecretId: string;
};
}
interface GetIdentityUniversalAuthClientSecretsEvent {
type: EventType.GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS;
metadata: {
identityId: string;
};
}
interface RevokeIdentityUniversalAuthClientSecretEvent {
type: EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET ;
metadata: {
identityId: string;
clientSecretId: string;
}; };
} }
@ -495,9 +551,16 @@ export type Event =
| DeleteTrustedIPEvent | DeleteTrustedIPEvent
| CreateServiceTokenEvent | CreateServiceTokenEvent
| DeleteServiceTokenEvent | DeleteServiceTokenEvent
| CreateServiceTokenV3Event | CreateIdentityEvent
| UpdateServiceTokenV3Event | UpdateIdentityEvent
| DeleteServiceTokenV3Event | DeleteIdentityEvent
| LoginIdentityUniversalAuthEvent
| AddIdentityUniversalAuthEvent
| UpdateIdentityUniversalAuthEvent
| GetIdentityUniversalAuthEvent
| CreateIdentityUniversalAuthClientSecretEvent
| GetIdentityUniversalAuthClientSecretsEvent
| RevokeIdentityUniversalAuthClientSecretEvent
| CreateEnvironmentEvent | CreateEnvironmentEvent
| UpdateEnvironmentEvent | UpdateEnvironmentEvent
| DeleteEnvironmentEvent | DeleteEnvironmentEvent

View File

@ -0,0 +1,31 @@
import express from "express";
const router = express.Router();
import { requireAuth } from "../../../middleware";
import { AuthMode } from "../../../variables";
import { identitiesController } from "../../controllers/v1";
router.post(
"/",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]
}),
identitiesController.createIdentity
);
router.patch(
"/:identityId",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
identitiesController.updateIdentity
);
router.delete(
"/:identityId",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
identitiesController.deleteIdentity
);
export default router;

View File

@ -1,3 +1,4 @@
import identities from "./identities";
import secret from "./secret"; import secret from "./secret";
import secretSnapshot from "./secretSnapshot"; import secretSnapshot from "./secretSnapshot";
import organizations from "./organizations"; import organizations from "./organizations";
@ -13,6 +14,7 @@ import secretRotationProvider from "./secretRotationProvider";
import secretRotation from "./secretRotation"; import secretRotation from "./secretRotation";
export { export {
identities,
secret, secret,
secretSnapshot, secretSnapshot,
organizations, organizations,

View File

@ -7,7 +7,7 @@ import { workspaceController } from "../../controllers/v1";
router.get( router.get(
"/:workspaceId/secret-snapshots", "/:workspaceId/secret-snapshots",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]
}), }),
workspaceController.getWorkspaceSecretSnapshots workspaceController.getWorkspaceSecretSnapshots
); );
@ -23,7 +23,7 @@ router.get(
router.post( router.post(
"/:workspaceId/secret-snapshots/rollback", "/:workspaceId/secret-snapshots/rollback",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]
}), }),
workspaceController.rollbackWorkspaceSecretSnapshot workspaceController.rollbackWorkspaceSecretSnapshot
); );
@ -31,7 +31,7 @@ router.post(
router.get( router.get(
"/:workspaceId/audit-logs", "/:workspaceId/audit-logs",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]
}), }),
workspaceController.getWorkspaceAuditLogs workspaceController.getWorkspaceAuditLogs
); );

View File

@ -1,7 +1,5 @@
import serviceTokenData from "./serviceTokenData";
import apiKeyData from "./apiKeyData"; import apiKeyData from "./apiKeyData";
export { export {
serviceTokenData,
apiKeyData apiKeyData
} }

View File

@ -1,44 +0,0 @@
import express from "express";
const router = express.Router();
import { requireAuth } from "../../../middleware";
import { AuthMode } from "../../../variables";
import { serviceTokenDataController } from "../../controllers/v3";
router.get(
"/me/key",
requireAuth({
acceptedAuthModes: [AuthMode.SERVICE_ACCESS_TOKEN]
}),
serviceTokenDataController.getServiceTokenDataKey
);
router.post(
"/me/token",
serviceTokenDataController.refreshToken
);
router.post(
"/",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
serviceTokenDataController.createServiceTokenData
);
router.patch(
"/:serviceTokenDataId",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
serviceTokenDataController.updateServiceTokenData
);
router.delete(
"/:serviceTokenDataId",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
serviceTokenDataController.deleteServiceTokenData
);
export default router;

View File

@ -3,7 +3,6 @@ import { AuditLog, Event } from "../models";
import { AuthData } from "../../interfaces/middleware"; import { AuthData } from "../../interfaces/middleware";
import EELicenseService from "./EELicenseService"; import EELicenseService from "./EELicenseService";
import { Workspace } from "../../models"; import { Workspace } from "../../models";
import { OrganizationNotFoundError } from "../../utils/errors";
interface EventScope { interface EventScope {
workspaceId?: Types.ObjectId; workspaceId?: Types.ObjectId;
@ -14,31 +13,42 @@ type ValidEventScope =
| Required<Pick<EventScope, "workspaceId">> | Required<Pick<EventScope, "workspaceId">>
| Required<Pick<EventScope, "organizationId">> | Required<Pick<EventScope, "organizationId">>
| Required<EventScope> | Required<EventScope>
| Record<string, never>;
export default class EEAuditLogService { export default class EEAuditLogService {
static async createAuditLog(authData: AuthData, event: Event, eventScope: ValidEventScope, shouldSave = true) { static async createAuditLog(authData: AuthData, event: Event, eventScope: ValidEventScope = {}, shouldSave = true) {
const MS_IN_DAY = 24 * 60 * 60 * 1000; const MS_IN_DAY = 24 * 60 * 60 * 1000;
const organizationId = ("organizationId" in eventScope) let organizationId;
? eventScope.organizationId if ("organizationId" in eventScope) {
: (await Workspace.findById(eventScope.workspaceId).select("organization").lean())?.organization; organizationId = eventScope.organizationId;
}
if (!organizationId) throw OrganizationNotFoundError({ let workspaceId;
message: "createAuditLog: Failed to create audit log due to missing organizationId" if ("workspaceId" in eventScope) {
}); workspaceId = eventScope.workspaceId;
if (!organizationId) {
organizationId = (await Workspace.findById(workspaceId).select("organization").lean())?.organization;
}
}
let expiresAt;
if (organizationId) {
const ttl = (await EELicenseService.getPlan(organizationId)).auditLogsRetentionDays * MS_IN_DAY; const ttl = (await EELicenseService.getPlan(organizationId)).auditLogsRetentionDays * MS_IN_DAY;
expiresAt = new Date(Date.now() + ttl);
}
const auditLog = await new AuditLog({ const auditLog = await new AuditLog({
actor: authData.actor, actor: authData.actor,
organization: organizationId, organization: organizationId,
workspace: ("workspaceId" in eventScope) ? eventScope.workspaceId : undefined, workspace: workspaceId,
ipAddress: authData.ipAddress, ipAddress: authData.ipAddress,
event, event,
userAgent: authData.userAgent, userAgent: authData.userAgent,
userAgentType: authData.userAgentType, userAgentType: authData.userAgentType,
expiresAt: new Date(Date.now() + ttl) expiresAt
}); });
if (shouldSave) { if (shouldSave) {

View File

@ -11,10 +11,15 @@ import { UnauthorizedRequestError } from "../../utils/errors";
import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js"; import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js";
import picomatch from "picomatch"; import picomatch from "picomatch";
import { AuthData } from "../../interfaces/middleware"; import { AuthData } from "../../interfaces/middleware";
import { ActorType, IRole } from "../models"; import { ActorType, IRole, Role } from "../models";
import { Membership, ServiceTokenData, ServiceTokenDataV3 } from "../../models"; import {
import { ADMIN, CUSTOM, MEMBER, VIEWER } from "../../variables"; IIdentity,
import { checkIPAgainstBlocklist } from "../../utils/ip"; IdentityMembership,
Membership,
ServiceTokenData
} from "../../models";
import { ADMIN, CUSTOM, MEMBER, NO_ACCESS, VIEWER } from "../../variables";
import { BadRequestError } from "../../utils/errors";
const $glob: FieldInstruction<string> = { const $glob: FieldInstruction<string> = {
type: "field", type: "field",
@ -55,7 +60,8 @@ export enum ProjectPermissionSub {
Secrets = "secrets", Secrets = "secrets",
SecretRollback = "secret-rollback", SecretRollback = "secret-rollback",
SecretApproval = "secret-approval", SecretApproval = "secret-approval",
SecretRotation = "secret-rotation" SecretRotation = "secret-rotation",
Identity = "identity"
} }
type SubjectFields = { type SubjectFields = {
@ -80,6 +86,7 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens] | [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval] | [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation] | [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
| [ProjectPermissionActions, ProjectPermissionSub.Identity]
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace] | [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace] | [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace]
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback] | [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
@ -126,6 +133,11 @@ const buildAdminPermission = () => {
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks); can(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks); can(ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens); can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens); can(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens); can(ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens);
@ -191,6 +203,11 @@ const buildMemberPermission = () => {
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks); can(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks); can(ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens); can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens); can(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens); can(ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens);
@ -231,6 +248,7 @@ const buildViewerPermission = () => {
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role); can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens); can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings); can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments); can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments);
@ -243,6 +261,13 @@ const buildViewerPermission = () => {
export const viewerProjectPermission = buildViewerPermission(); export const viewerProjectPermission = buildViewerPermission();
const buildNoAccessProjectPermission = () => {
const { build } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
return build({ conditionsMatcher });
}
export const noAccessProjectPermissions = buildNoAccessProjectPermission();
/** /**
* Return permissions for user/service pertaining to workspace with id [workspaceId] * Return permissions for user/service pertaining to workspace with id [workspaceId]
* *
@ -256,7 +281,7 @@ export const getAuthDataProjectPermissions = async ({
authData: AuthData; authData: AuthData;
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
}) => { }) => {
let role: "admin" | "member" | "viewer" | "custom"; let role: "admin" | "member" | "viewer" | "no-access" | "custom";
let customRole; let customRole;
switch (authData.actor.type) { switch (authData.actor.type) {
@ -284,25 +309,24 @@ export const getAuthDataProjectPermissions = async ({
role = "viewer"; role = "viewer";
break; break;
} }
case ActorType.SERVICE_V3: { case ActorType.IDENTITY: {
const serviceTokenData = await ServiceTokenDataV3 const identityMembership = await IdentityMembership.findOne({
.findById(authData.authPayload._id) identity: authData.authPayload._id,
workspace: workspaceId
})
.populate<{ .populate<{
customRole: IRole & { permissions: RawRuleOf<MongoAbility<ProjectPermissionSet>>[] }; customRole: IRole & { permissions: RawRuleOf<MongoAbility<ProjectPermissionSet>>[] };
}>("customRole") identity: IIdentity
}>("customRole identity")
.exec(); .exec();
if (!serviceTokenData || (serviceTokenData.role === "custom" && !serviceTokenData.customRole)) { if (!identityMembership || (identityMembership.role === "custom" && !identityMembership.customRole)) {
throw UnauthorizedRequestError(); throw UnauthorizedRequestError();
} }
checkIPAgainstBlocklist({ role = identityMembership.role;
ipAddress: authData.ipAddress, customRole = identityMembership.customRole;
trustedIps: serviceTokenData.trustedIps
});
role = serviceTokenData.role;
customRole = serviceTokenData.customRole;
break; break;
} }
default: default:
@ -316,6 +340,8 @@ export const getAuthDataProjectPermissions = async ({
return { permission: memberProjectPermissions }; return { permission: memberProjectPermissions };
case VIEWER: case VIEWER:
return { permission: viewerProjectPermission }; return { permission: viewerProjectPermission };
case NO_ACCESS:
return { permission: noAccessProjectPermissions };
case CUSTOM: { case CUSTOM: {
if (!customRole) throw UnauthorizedRequestError(); if (!customRole) throw UnauthorizedRequestError();
return { return {
@ -329,3 +355,61 @@ export const getAuthDataProjectPermissions = async ({
throw UnauthorizedRequestError(); throw UnauthorizedRequestError();
} }
} }
export const getWorkspaceRolePermissions = async (role: string, workspaceId: string) => {
const isCustomRole = ![ADMIN, MEMBER, VIEWER, NO_ACCESS].includes(role);
if (isCustomRole) {
const workspaceRole = await Role.findOne({
slug: role,
isOrgRole: false,
workspace: new Types.ObjectId(workspaceId)
});
if (!workspaceRole) throw BadRequestError({ message: "Role not found" });
return createMongoAbility<ProjectPermissionSet>(workspaceRole.permissions as RawRuleOf<MongoAbility<ProjectPermissionSet>>[], {
conditionsMatcher
});
}
switch (role) {
case ADMIN:
return adminProjectPermissions;
case MEMBER:
return memberProjectPermissions;
case VIEWER:
return viewerProjectPermission;
case NO_ACCESS:
return noAccessProjectPermissions;
default:
throw BadRequestError({ message: "Role not found" });
}
}
/**
* Extracts and formats permissions from a CASL Ability object or a raw permission set.
* @param ability
* @returns
*/
const extractPermissions = (ability: any) => {
return ability.A.map((permission: any) => `${permission.action}_${permission.subject}`);
}
/**
* Compares two sets of permissions to determine if the first set is at least as privileged as the second set.
* The function checks if all permissions in the second set are contained within the first set and if the first set has equal or more permissions.
*
*/
export const isAtLeastAsPrivilegedWorkspace = (permissions1: MongoAbility<ProjectPermissionSet> | ProjectPermissionSet, permissions2: MongoAbility<ProjectPermissionSet> | ProjectPermissionSet) => {
const set1 = new Set(extractPermissions(permissions1));
const set2 = new Set(extractPermissions(permissions2));
for (const perm of set2) {
if (!set1.has(perm)) {
return false;
}
}
return set1.size >= set2.size;
}

View File

@ -1,9 +1,15 @@
import { Types } from "mongoose";
import { AbilityBuilder, MongoAbility, RawRuleOf, createMongoAbility } from "@casl/ability"; import { AbilityBuilder, MongoAbility, RawRuleOf, createMongoAbility } from "@casl/ability";
import { MembershipOrg } from "../../models"; import {
import { IRole } from "../models/role"; IIdentity,
IdentityMembershipOrg,
MembershipOrg
} from "../../models";
import { ActorType, IRole, Role } from "../models";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import { ACCEPTED } from "../../variables"; import { ACCEPTED, ADMIN, CUSTOM, MEMBER, NO_ACCESS} from "../../variables";
import { conditionsMatcher } from "./ProjectRoleService"; import { conditionsMatcher } from "./ProjectRoleService";
import { AuthData } from "../../interfaces/middleware";
export enum OrgPermissionActions { export enum OrgPermissionActions {
Read = "read", Read = "read",
@ -20,7 +26,8 @@ export enum OrgPermissionSubjects {
IncidentAccount = "incident-contact", IncidentAccount = "incident-contact",
Sso = "sso", Sso = "sso",
Billing = "billing", Billing = "billing",
SecretScanning = "secret-scanning" SecretScanning = "secret-scanning",
Identity = "identity"
} }
export type OrgPermissionSet = export type OrgPermissionSet =
@ -32,7 +39,8 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount] | [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount]
| [OrgPermissionActions, OrgPermissionSubjects.Sso] | [OrgPermissionActions, OrgPermissionSubjects.Sso]
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning] | [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
| [OrgPermissionActions, OrgPermissionSubjects.Billing]; | [OrgPermissionActions, OrgPermissionSubjects.Billing]
| [OrgPermissionActions, OrgPermissionSubjects.Identity];
const buildAdminPermission = () => { const buildAdminPermission = () => {
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility); const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
@ -75,6 +83,11 @@ const buildAdminPermission = () => {
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing); can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Billing); can(OrgPermissionActions.Delete, OrgPermissionSubjects.Billing);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity);
return build({ conditionsMatcher }); return build({ conditionsMatcher });
}; };
@ -98,13 +111,26 @@ const buildMemberPermission = () => {
can(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning); can(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.SecretScanning); can(OrgPermissionActions.Delete, OrgPermissionSubjects.SecretScanning);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity);
return build({ conditionsMatcher }); return build({ conditionsMatcher });
}; };
export const memberPermissions = buildMemberPermission(); export const memberPermissions = buildMemberPermission();
const buildNoAccessPermission = () => {
const { build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
return build({ conditionsMatcher });
}
export const noAccessPermissions = buildNoAccessPermission();
export const getUserOrgPermissions = async (userId: string, orgId: string) => { export const getUserOrgPermissions = async (userId: string, orgId: string) => {
// TODO(akhilmhdh): speed this up by pulling from cache later // TODO(akhilmhdh): speed this up by pulling from cache later
const membership = await MembershipOrg.findOne({ const membership = await MembershipOrg.findOne({
user: userId, user: userId,
organization: orgId, organization: orgId,
@ -119,11 +145,13 @@ export const getUserOrgPermissions = async (userId: string, orgId: string) => {
throw UnauthorizedRequestError({ message: "User doesn't belong to organization" }); throw UnauthorizedRequestError({ message: "User doesn't belong to organization" });
} }
if (membership.role === "admin") return { permission: adminPermissions, membership }; if (membership.role === ADMIN) return { permission: adminPermissions, membership };
if (membership.role === "member") return { permission: memberPermissions, membership }; if (membership.role === MEMBER) return { permission: memberPermissions, membership };
if (membership.role === "custom") { if (membership.role === NO_ACCESS) return { permission: noAccessPermissions, membership }
if (membership.role === CUSTOM) {
const permission = createMongoAbility<OrgPermissionSet>(membership.customRole.permissions, { const permission = createMongoAbility<OrgPermissionSet>(membership.customRole.permissions, {
conditionsMatcher conditionsMatcher
}); });
@ -132,3 +160,142 @@ export const getUserOrgPermissions = async (userId: string, orgId: string) => {
throw BadRequestError({ message: "User role not found" }); throw BadRequestError({ message: "User role not found" });
}; };
/**
* Return permissions for user/service pertaining to organization with id [organizationId]
*
* Note: should not rely on this function for ST V2 authorization logic
* b/c ST V2 does not support role-based access control but also not organization-level resources
*/
export const getAuthDataOrgPermissions = async ({
authData,
organizationId
}: {
authData: AuthData;
organizationId: Types.ObjectId;
}) => {
let role: "admin" | "member" | "no-access" | "custom";
let customRole;
switch (authData.actor.type) {
case ActorType.USER: {
const membershipOrg = await MembershipOrg.findOne({
user: authData.authPayload._id,
organization: organizationId,
status: ACCEPTED
})
.populate<{ customRole: IRole & { permissions: RawRuleOf<MongoAbility<OrgPermissionSet>>[] } }>(
"customRole"
)
.exec();
if (!membershipOrg || (membershipOrg.role === "custom" && !membershipOrg.customRole)) {
throw UnauthorizedRequestError({ message: "User doesn't belong to organization" });
}
role = membershipOrg.role;
customRole = membershipOrg.customRole;
break;
}
case ActorType.SERVICE: {
throw UnauthorizedRequestError({
message: "Failed to access organization-level resources with service token"
});
}
case ActorType.IDENTITY: {
const identityMembershipOrg = await IdentityMembershipOrg.findOne({
identity: authData.authPayload._id,
organization: organizationId
})
.populate<{
customRole: IRole & { permissions: RawRuleOf<MongoAbility<OrgPermissionSet>>[] };
identity: IIdentity
}>("customRole identity")
.exec();
if (!identityMembershipOrg || (identityMembershipOrg.role === "custom" && !identityMembershipOrg.customRole)) {
throw UnauthorizedRequestError();
}
role = identityMembershipOrg.role;
customRole = identityMembershipOrg.customRole;
break;
}
default:
throw UnauthorizedRequestError();
}
switch (role) {
case ADMIN:
return { permission: adminPermissions };
case MEMBER:
return { permission: memberPermissions };
case NO_ACCESS:
return { permission: noAccessPermissions };
case CUSTOM: {
if (!customRole) throw UnauthorizedRequestError();
return {
permission: createMongoAbility<OrgPermissionSet>(
customRole.permissions,
{ conditionsMatcher }
)
};
}
}
}
export const getOrgRolePermissions = async (role: string, orgId: string) => {
const isCustomRole = ![ADMIN, MEMBER, NO_ACCESS].includes(role);
if (isCustomRole) {
const orgRole = await Role.findOne({
slug: role,
isOrgRole: true,
organization: new Types.ObjectId(orgId)
});
if (!orgRole) throw BadRequestError({ message: "Org Role not found" });
return createMongoAbility<OrgPermissionSet>(orgRole.permissions as RawRuleOf<MongoAbility<OrgPermissionSet>>[], {
conditionsMatcher
});
}
switch (role) {
case ADMIN:
return adminPermissions;
case MEMBER:
return memberPermissions;
case NO_ACCESS:
return noAccessPermissions;
default:
throw BadRequestError({ message: "User org role not found" });
}
}
/**
* Extracts and formats permissions from a CASL Ability object or a raw permission set.
* @param ability
* @returns
*/
const extractPermissions = (ability: any) => {
return ability.A.map((permission: any) => `${permission.action}_${permission.subject}`);
}
/**
* Compares two sets of permissions to determine if the first set is at least as privileged as the second set.
* The function checks if all permissions in the second set are contained within the first set and if the first set has equal or more permissions.
*
*/
export const isAtLeastAsPrivilegedOrg = (permissions1: MongoAbility<OrgPermissionSet> | OrgPermissionSet, permissions2: MongoAbility<OrgPermissionSet> | OrgPermissionSet) => {
const set1 = new Set(extractPermissions(permissions1));
const set2 = new Set(extractPermissions(permissions2));
for (const perm of set2) {
if (!set1.has(perm)) {
return false;
}
}
return set1.size >= set2.size;
}

View File

@ -13,7 +13,7 @@ import {
SECRET_SHARED SECRET_SHARED
} from "../variables"; } from "../variables";
import { client, getEncryptionKey, getRootEncryptionKey } from "../config"; import { client, getEncryptionKey, getRootEncryptionKey } from "../config";
import { InternalServerError } from "../utils/errors"; import { BotNotFoundError, InternalServerError } from "../utils/errors";
import { Folder } from "../models"; import { Folder } from "../models";
import { getFolderByPath } from "../services/FolderService"; import { getFolderByPath } from "../services/FolderService";
import { getAllImportedSecrets } from "../services/SecretImportService"; import { getAllImportedSecrets } from "../services/SecretImportService";
@ -223,7 +223,7 @@ export const getKey = async ({ workspaceId }: { workspaceId: Types.ObjectId }) =
workspace: workspaceId workspace: workspaceId
}).populate<{ sender: IUser }>("sender", "publicKey"); }).populate<{ sender: IUser }>("sender", "publicKey");
if (!botKey) throw new Error("Failed to find bot key"); if (!botKey) throw BotNotFoundError({ message: `getKey: Failed to find bot key for [workspaceId=${workspaceId}]` })
const bot = await Bot.findOne({ const bot = await Bot.findOne({
workspace: workspaceId workspace: workspaceId

View File

@ -17,7 +17,7 @@ export const validateMembership = async ({
}: { }: {
userId: Types.ObjectId | string; userId: Types.ObjectId | string;
workspaceId: Types.ObjectId | string; workspaceId: Types.ObjectId | string;
acceptedRoles?: Array<"admin" | "member" | "custom" | "viewer">; acceptedRoles?: Array<"admin" | "member" | "custom" | "viewer" | "no-access">;
}) => { }) => {
const membership = await Membership.findOne({ const membership = await Membership.findOne({
user: userId, user: userId,

View File

@ -18,7 +18,7 @@ export const validateMembershipOrg = async ({
}: { }: {
userId: Types.ObjectId; userId: Types.ObjectId;
organizationId: Types.ObjectId; organizationId: Types.ObjectId;
acceptedRoles?: Array<"owner" | "admin" | "member" | "custom">; acceptedRoles?: Array<"owner" | "admin" | "member" | "custom" | "no-access">;
acceptedStatuses?: Array<"invited" | "accepted">; acceptedStatuses?: Array<"invited" | "accepted">;
}) => { }) => {
const membershipOrg = await MembershipOrg.findOne({ const membershipOrg = await MembershipOrg.findOne({

View File

@ -4,6 +4,11 @@ import {
BotKey, BotKey,
BotOrg, BotOrg,
Folder, Folder,
Identity,
IdentityMembership,
IdentityMembershipOrg,
IdentityUniversalAuth,
IdentityUniversalAuthClientSecret,
IncidentContactOrg, IncidentContactOrg,
Integration, Integration,
IntegrationAuth, IntegrationAuth,
@ -16,8 +21,6 @@ import {
SecretImport, SecretImport,
ServiceToken, ServiceToken,
ServiceTokenData, ServiceTokenData,
ServiceTokenDataV3,
ServiceTokenDataV3Key,
Tag, Tag,
Webhook, Webhook,
Workspace Workspace
@ -124,6 +127,32 @@ export const deleteOrganization = async ({
organization: organization._id organization: organization._id
}); });
const identityIds = await IdentityMembershipOrg.distinct("identity", {
organization: organization._id
});
await IdentityMembershipOrg.deleteMany({
organization: organization._id
});
await Identity.deleteMany({
_id: {
$in: identityIds
}
});
await IdentityUniversalAuth.deleteMany({
identity: {
$in: identityIds
}
});
await IdentityUniversalAuthClientSecret.deleteMany({
identity: {
$in: identityIds
}
});
await BotOrg.deleteMany({ await BotOrg.deleteMany({
organization: organization._id organization: organization._id
}); });
@ -268,13 +297,7 @@ export const deleteOrganization = async ({
} }
}); });
await ServiceTokenDataV3.deleteMany({ await IdentityMembership.deleteMany({
workspace: {
$in: workspaceIds
}
});
await ServiceTokenDataV3Key.deleteMany({
workspace: { workspace: {
$in: workspaceIds $in: workspaceIds
} }

View File

@ -3,6 +3,7 @@ import {
Bot, Bot,
BotKey, BotKey,
Folder, Folder,
IdentityMembership,
Integration, Integration,
IntegrationAuth, IntegrationAuth,
Key, Key,
@ -12,8 +13,6 @@ import {
SecretImport, SecretImport,
ServiceToken, ServiceToken,
ServiceTokenData, ServiceTokenData,
ServiceTokenDataV3,
ServiceTokenDataV3Key,
Tag, Tag,
Webhook, Webhook,
Workspace Workspace
@ -179,11 +178,7 @@ export const deleteWorkspace = async ({
workspace: workspace._id workspace: workspace._id
}); });
await ServiceTokenDataV3.deleteMany({ await IdentityMembership.deleteMany({
workspace: workspace._id
});
await ServiceTokenDataV3Key.deleteMany({
workspace: workspace._id workspace: workspace._id
}); });

View File

@ -25,6 +25,7 @@ import {
secretSnapshot as eeSecretSnapshotRouter, secretSnapshot as eeSecretSnapshotRouter,
users as eeUsersRouter, users as eeUsersRouter,
workspace as eeWorkspaceRouter, workspace as eeWorkspaceRouter,
identities as v1IdentitiesRouter,
roles as v1RoleRouter, roles as v1RoleRouter,
secretApprovalPolicy as v1SecretApprovalPolicyRouter, secretApprovalPolicy as v1SecretApprovalPolicyRouter,
secretApprovalRequest as v1SecretApprovalRequestRouter, secretApprovalRequest as v1SecretApprovalRequestRouter,
@ -33,7 +34,6 @@ import {
secretScanning as v1SecretScanningRouter secretScanning as v1SecretScanningRouter
} from "./ee/routes/v1"; } from "./ee/routes/v1";
import { apiKeyData as v3apiKeyDataRouter } from "./ee/routes/v3"; import { apiKeyData as v3apiKeyDataRouter } from "./ee/routes/v3";
import { serviceTokenData as v3ServiceTokenDataRouter } from "./ee/routes/v3";
import { import {
admin as v1AdminRouter, admin as v1AdminRouter,
auth as v1AuthRouter, auth as v1AuthRouter,
@ -52,6 +52,7 @@ import {
secretsFolder as v1SecretsFolder, secretsFolder as v1SecretsFolder,
serviceToken as v1ServiceTokenRouter, serviceToken as v1ServiceTokenRouter,
signup as v1SignupRouter, signup as v1SignupRouter,
universalAuth as v1UniversalAuthRouter,
userAction as v1UserActionRouter, userAction as v1UserActionRouter,
user as v1UserRouter, user as v1UserRouter,
webhooks as v1WebhooksRouter, webhooks as v1WebhooksRouter,
@ -198,6 +199,7 @@ const main = async () => {
} }
// (EE) routes // (EE) routes
app.use("/api/v1/identities", v1IdentitiesRouter);
app.use("/api/v1/secret", eeSecretRouter); app.use("/api/v1/secret", eeSecretRouter);
app.use("/api/v1/secret-snapshot", eeSecretSnapshotRouter); app.use("/api/v1/secret-snapshot", eeSecretSnapshotRouter);
app.use("/api/v1/users", eeUsersRouter); app.use("/api/v1/users", eeUsersRouter);
@ -205,14 +207,14 @@ const main = async () => {
app.use("/api/v1/organizations", eeOrganizationsRouter); app.use("/api/v1/organizations", eeOrganizationsRouter);
app.use("/api/v1/sso", eeSSORouter); app.use("/api/v1/sso", eeSSORouter);
app.use("/api/v1/cloud-products", eeCloudProductsRouter); app.use("/api/v1/cloud-products", eeCloudProductsRouter);
app.use("/api/v3/api-key", v3apiKeyDataRouter); // new app.use("/api/v3/api-key", v3apiKeyDataRouter);
app.use("/api/v3/service-token", v3ServiceTokenDataRouter); // new
app.use("/api/v1/secret-rotation-providers", v1SecretRotationProviderRouter); app.use("/api/v1/secret-rotation-providers", v1SecretRotationProviderRouter);
app.use("/api/v1/secret-rotations", v1SecretRotation); app.use("/api/v1/secret-rotations", v1SecretRotation);
// v1 routes // v1 routes
app.use("/api/v1/signup", v1SignupRouter); app.use("/api/v1/signup", v1SignupRouter);
app.use("/api/v1/auth", v1AuthRouter); app.use("/api/v1/auth", v1AuthRouter);
app.use("/api/v1/auth", v1UniversalAuthRouter); // new
app.use("/api/v1/admin", v1AdminRouter); app.use("/api/v1/admin", v1AdminRouter);
app.use("/api/v1/bot", v1BotRouter); app.use("/api/v1/bot", v1BotRouter);
app.use("/api/v1/user", v1UserRouter); app.use("/api/v1/user", v1UserRouter);
@ -220,7 +222,7 @@ const main = async () => {
app.use("/api/v1/organization", v1OrganizationRouter); app.use("/api/v1/organization", v1OrganizationRouter);
app.use("/api/v1/workspace", v1WorkspaceRouter); app.use("/api/v1/workspace", v1WorkspaceRouter);
app.use("/api/v1/membership-org", v1MembershipOrgRouter); app.use("/api/v1/membership-org", v1MembershipOrgRouter);
app.use("/api/v1/membership", v1MembershipRouter); // app.use("/api/v1/membership", v1MembershipRouter);
app.use("/api/v1/key", v1KeyRouter); app.use("/api/v1/key", v1KeyRouter);
app.use("/api/v1/invite-org", v1InviteOrgRouter); app.use("/api/v1/invite-org", v1InviteOrgRouter);
app.use("/api/v1/secret", v1SecretRouter); // deprecate app.use("/api/v1/secret", v1SecretRouter); // deprecate
@ -247,7 +249,7 @@ const main = async () => {
app.use("/api/v2/workspace", v2TagsRouter); app.use("/api/v2/workspace", v2TagsRouter);
app.use("/api/v2/workspace", v2WorkspaceRouter); app.use("/api/v2/workspace", v2WorkspaceRouter);
app.use("/api/v2/secret", v2SecretRouter); // deprecate app.use("/api/v2/secret", v2SecretRouter); // deprecate
app.use("/api/v2/secrets", v2SecretsRouter); // note: in the process of moving to v3/secrets app.use("/api/v2/secrets", v2SecretsRouter);
app.use("/api/v2/service-token", v2ServiceTokenDataRouter); app.use("/api/v2/service-token", v2ServiceTokenDataRouter);
// v3 routes (experimental) // v3 routes (experimental)

View File

@ -1,6 +1,6 @@
import { Types } from "mongoose"; import { Types } from "mongoose";
import { IServiceTokenData, IServiceTokenDataV3, IUser } from "../../models"; import { IIdentity, IServiceTokenData, IUser } from "../../models";
import { ServiceActor, ServiceActorV3, UserActor, UserAgentType } from "../../ee/models"; import { IdentityActor, ServiceActor, UserActor, UserAgentType } from "../../ee/models";
interface BaseAuthData { interface BaseAuthData {
ipAddress: string; ipAddress: string;
@ -14,9 +14,9 @@ export interface UserAuthData extends BaseAuthData {
authPayload: IUser; authPayload: IUser;
} }
export interface ServiceTokenV3AuthData extends BaseAuthData { export interface IdentityAuthData extends BaseAuthData {
actor: ServiceActorV3; actor: IdentityActor;
authPayload: IServiceTokenDataV3; authPayload: IIdentity;
} }
export interface ServiceTokenAuthData extends BaseAuthData { export interface ServiceTokenAuthData extends BaseAuthData {
@ -24,4 +24,4 @@ export interface ServiceTokenAuthData extends BaseAuthData {
authPayload: IServiceTokenData; authPayload: IServiceTokenData;
} }
export type AuthData = UserAuthData | ServiceTokenV3AuthData | ServiceTokenAuthData; export type AuthData = UserAuthData | IdentityAuthData | ServiceTokenAuthData;

View File

@ -39,8 +39,9 @@ export const requestErrorHandler: ErrorRequestHandler = async (
Sentry.captureException(error); Sentry.captureException(error);
delete (<any>error).stacktrace // remove stack trace from being sent to client res.status((<RequestError>error).statusCode).send(
res.status((<RequestError>error).statusCode).json(error); // revise json part here await error.format(req)
);
next(); next();
}; };

View File

@ -50,7 +50,7 @@ const requireAuth = ({
case AuthMode.SERVICE_TOKEN: case AuthMode.SERVICE_TOKEN:
req.serviceTokenData = authData.authPayload; req.serviceTokenData = authData.authPayload;
break; break;
case AuthMode.SERVICE_ACCESS_TOKEN: case AuthMode.IDENTITY_ACCESS_TOKEN:
req.serviceTokenData = authData.authPayload; req.serviceTokenData = authData.authPayload;
break; break;
case AuthMode.API_KEY: case AuthMode.API_KEY:

View File

@ -0,0 +1,38 @@
import { Document, Schema, Types, model } from "mongoose";
import { IPType } from "../ee/models";
export interface IIdentityTrustedIp {
ipAddress: string;
type: IPType;
prefix: number;
}
export enum IdentityAuthMethod {
UNIVERSAL_AUTH = "universal-auth"
}
export interface IIdentity extends Document {
_id: Types.ObjectId;
name: string;
authMethod?: IdentityAuthMethod;
}
const identitySchema = new Schema(
{
name: {
type: String,
required: true
},
authMethod: {
type: String,
enum: IdentityAuthMethod,
required: false,
},
},
{
timestamps: true
}
);
export const Identity = model<IIdentity>("Identity", identitySchema);

View File

@ -0,0 +1,104 @@
import { Document, Schema, Types, model } from "mongoose";
import { IIdentityTrustedIp } from "./identity";
import { IPType } from "../ee/models/trustedIp";
export interface IIdentityAccessToken extends Document {
_id: Types.ObjectId;
identity: Types.ObjectId;
identityUniversalAuthClientSecret?: Types.ObjectId;
accessTokenLastUsedAt?: Date;
accessTokenLastRenewedAt?: Date;
accessTokenNumUses: number;
accessTokenNumUsesLimit: number;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenTrustedIps: Array<IIdentityTrustedIp>;
isAccessTokenRevoked: boolean;
updatedAt: Date;
createdAt: Date;
}
const identityAccessTokenSchema = new Schema(
{
identity: {
type: Schema.Types.ObjectId,
ref: "Identity",
required: false
},
identityUniversalAuthClientSecret: {
type: Schema.Types.ObjectId,
ref: "IdentityUniversalAuthClientSecret",
required: false
},
accessTokenLastUsedAt: {
type: Date,
required: false
},
accessTokenLastRenewedAt: {
type: Date,
required: false
},
accessTokenNumUses: {
// number of times access token has been used
type: Number,
default: 0,
required: true
},
accessTokenNumUsesLimit: {
// number of times access token can be used for
type: Number,
default: 0, // default: used as many times as needed
required: true
},
accessTokenTTL: { // seconds
// incremental lifetime
type: Number,
default: 2592000, // 30 days
required: true
},
accessTokenMaxTTL: { // seconds
// max lifetime
type: Number,
default: 2592000, // 30 days
required: true
},
accessTokenTrustedIps: {
type: [
{
ipAddress: {
type: String,
required: true
},
type: {
type: String,
enum: [
IPType.IPV4,
IPType.IPV6
],
required: true
},
prefix: {
type: Number,
required: false
}
}
],
default: [{
ipAddress: "0.0.0.0",
type: IPType.IPV4.toString(),
prefix: 0
}],
required: true
},
isAccessTokenRevoked: {
type: Boolean,
default: false,
required: true
},
},
{
timestamps: true
}
);
export const IdentityAccessToken = model<IIdentityAccessToken>("IdentityAccessToken", identityAccessTokenSchema);

View File

@ -0,0 +1,39 @@
import { Schema, Types, model } from "mongoose";
import { ADMIN, CUSTOM, MEMBER, NO_ACCESS, VIEWER } from "../variables";
export interface IIdentityMembership {
_id: Types.ObjectId;
identity: Types.ObjectId;
workspace: Types.ObjectId;
role: "admin" | "member" | "viewer" | "no-access" | "custom";
customRole: Types.ObjectId;
}
const identityMembershipSchema = new Schema<IIdentityMembership>(
{
identity: {
type: Schema.Types.ObjectId,
ref: "Identity"
},
workspace: {
type: Schema.Types.ObjectId,
ref: "Workspace",
required: true,
index: true,
},
role: {
type: String,
enum: [ADMIN, MEMBER, VIEWER, CUSTOM, NO_ACCESS],
required: true
},
customRole: {
type: Schema.Types.ObjectId,
ref: "Role"
}
},
{
timestamps: true
}
);
export const IdentityMembership = model<IIdentityMembership>("IdentityMembership", identityMembershipSchema);

View File

@ -0,0 +1,37 @@
import { Schema, Types, model } from "mongoose";
import { ADMIN, CUSTOM, MEMBER, NO_ACCESS} from "../variables";
export interface IIdentityMembershipOrg {
_id: Types.ObjectId;
identity: Types.ObjectId;
organization: Types.ObjectId;
role: "admin" | "member" | "no-access" | "custom";
customRole: Types.ObjectId;
}
const identityMembershipOrgSchema = new Schema<IIdentityMembershipOrg>(
{
identity: {
type: Schema.Types.ObjectId,
ref: "Identity"
},
organization: {
type: Schema.Types.ObjectId,
ref: "Organization"
},
role: {
type: String,
enum: [ADMIN, MEMBER, NO_ACCESS, CUSTOM],
required: true
},
customRole: {
type: Schema.Types.ObjectId,
ref: "Role"
}
},
{
timestamps: true
}
);
export const IdentityMembershipOrg = model<IIdentityMembershipOrg>("IdentityMembershipOrg", identityMembershipOrgSchema);

View File

@ -0,0 +1,107 @@
import { Document, Schema, Types, model } from "mongoose";
import { IPType } from "../ee/models";
import { IIdentityTrustedIp } from "./identity";
export interface IIdentityUniversalAuth extends Document {
_id: Types.ObjectId;
identity: Types.ObjectId;
clientId: string;
clientSecretTrustedIps: Array<IIdentityTrustedIp>;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: Array<IIdentityTrustedIp>;
}
const identityUniversalAuthSchema = new Schema(
{
identity: {
type: Schema.Types.ObjectId,
ref: "Identity",
required: true
},
clientId: {
type: String,
required: true
},
clientSecretTrustedIps: {
type: [
{
ipAddress: {
type: String,
required: true
},
type: {
type: String,
enum: [
IPType.IPV4,
IPType.IPV6
],
required: true
},
prefix: {
type: Number,
required: false
}
}
],
default: [{
ipAddress: "0.0.0.0",
type: IPType.IPV4.toString(),
prefix: 0
}],
required: true
},
accessTokenTTL: { // seconds
// incremental lifetime
type: Number,
default: 7200,
required: true
},
accessTokenMaxTTL: { // seconds
// max lifetime
type: Number,
default: 7200,
required: true
},
accessTokenNumUsesLimit: {
// number of times access token can be used for
type: Number,
default: 0, // default: used as many times as needed
required: true
},
accessTokenTrustedIps: {
type: [
{
ipAddress: {
type: String,
required: true
},
type: {
type: String,
enum: [
IPType.IPV4,
IPType.IPV6
],
required: true
},
prefix: {
type: Number,
required: false
}
}
],
default: [{
ipAddress: "0.0.0.0",
type: IPType.IPV4.toString(),
prefix: 0
}],
required: true
}
},
{
timestamps: true
}
);
export const IdentityUniversalAuth = model<IIdentityUniversalAuth>("IdentityUniversalAuth", identityUniversalAuthSchema);

View File

@ -0,0 +1,81 @@
import { Document, Schema, Types, model } from "mongoose";
export interface IIdentityUniversalAuthClientSecret extends Document {
_id: Types.ObjectId;
identity: Types.ObjectId;
identityUniversalAuth : Types.ObjectId;
description: string;
clientSecretPrefix: string;
clientSecretHash: string;
clientSecretLastUsedAt?: Date;
clientSecretNumUses: number;
clientSecretNumUsesLimit: number;
clientSecretTTL: number;
updatedAt: Date;
createdAt: Date;
isClientSecretRevoked: boolean;
}
const identityUniversalAuthClientSecretSchema = new Schema(
{
identity: {
type: Schema.Types.ObjectId,
ref: "Identity",
required: true
},
identityUniversalAuth: {
type: Schema.Types.ObjectId,
ref: "IdentityUniversalAuth",
required: true
},
description: {
type: String,
required: true
},
clientSecretPrefix: {
type: String,
required: true
},
clientSecretHash: {
type: String,
required: true
},
clientSecretLastUsedAt: {
type: Date,
required: false
},
clientSecretNumUses: {
// number of times client secret has been used
// in login operation
type: Number,
default: 0,
required: true
},
clientSecretNumUsesLimit: {
// number of times client secret can be used for
// a login operation
type: Number,
default: 0, // default: used as many times as needed
required: true
},
clientSecretTTL: {
type: Number,
default: 0, // default: does not expire
required: true
},
isClientSecretRevoked: {
type: Boolean,
default: false,
required: true
}
},
{
timestamps: true
}
);
identityUniversalAuthClientSecretSchema.index(
{ identityUniversalAuth: 1, isClientSecretRevoked: 1 }
);
export const IdentityUniversalAuthClientSecret = model<IIdentityUniversalAuthClientSecret>("IdentityUniversalAuthClientSecret", identityUniversalAuthClientSecretSchema);

View File

@ -20,8 +20,15 @@ export * from "./user";
export * from "./userAction"; export * from "./userAction";
export * from "./workspace"; export * from "./workspace";
export * from "./serviceTokenData"; // TODO: deprecate export * from "./serviceTokenData"; // TODO: deprecate
export * from "./serviceTokenDataV3";
export * from "./serviceTokenDataV3Key"; // new
export * from "./identity";
export * from "./identityMembership";
export * from "./identityMembershipOrg";
export * from "./identityUniversalAuth";
export * from "./identityUniversalAuthClientSecret";
export * from "./identityAccessToken";
export * from "./apiKeyData"; // TODO: deprecate export * from "./apiKeyData"; // TODO: deprecate
export * from "./apiKeyDataV2"; export * from "./apiKeyDataV2";
export * from "./loginSRPDetail"; export * from "./loginSRPDetail";

View File

@ -1,5 +1,5 @@
import { Schema, Types, model } from "mongoose"; import { Schema, Types, model } from "mongoose";
import { ADMIN, CUSTOM, MEMBER, VIEWER } from "../variables"; import { ADMIN, CUSTOM, MEMBER, NO_ACCESS, VIEWER } from "../variables";
export interface IMembershipPermission { export interface IMembershipPermission {
environmentSlug: string; environmentSlug: string;
@ -11,7 +11,7 @@ export interface IMembership {
user: Types.ObjectId; user: Types.ObjectId;
inviteEmail?: string; inviteEmail?: string;
workspace: Types.ObjectId; workspace: Types.ObjectId;
role: "admin" | "member" | "viewer" | "custom"; role: "admin" | "member" | "viewer" | "no-access" | "custom";
customRole: Types.ObjectId; customRole: Types.ObjectId;
deniedPermissions: IMembershipPermission[]; deniedPermissions: IMembershipPermission[];
} }
@ -44,7 +44,7 @@ const membershipSchema = new Schema<IMembership>(
}, },
role: { role: {
type: String, type: String,
enum: [ADMIN, MEMBER, VIEWER, CUSTOM], enum: [ADMIN, MEMBER, VIEWER, NO_ACCESS, CUSTOM],
required: true required: true
}, },
customRole: { customRole: {

View File

@ -1,12 +1,12 @@
import { Document, Schema, Types, model } from "mongoose"; import { Document, Schema, Types, model } from "mongoose";
import { ACCEPTED, ADMIN, CUSTOM, INVITED, MEMBER } from "../variables"; import { ACCEPTED, ADMIN, CUSTOM, INVITED, MEMBER, NO_ACCESS } from "../variables";
export interface IMembershipOrg extends Document { export interface IMembershipOrg extends Document {
_id: Types.ObjectId; _id: Types.ObjectId;
user: Types.ObjectId; user: Types.ObjectId;
inviteEmail: string; inviteEmail: string;
organization: Types.ObjectId; organization: Types.ObjectId;
role: "owner" | "admin" | "member" | "custom"; role: "admin" | "member" | "no-access" | "custom";
customRole: Types.ObjectId; customRole: Types.ObjectId;
status: "invited" | "accepted"; status: "invited" | "accepted";
} }
@ -26,7 +26,7 @@ const membershipOrgSchema = new Schema(
}, },
role: { role: {
type: String, type: String,
enum: [ADMIN, MEMBER, CUSTOM], enum: [ADMIN, MEMBER, NO_ACCESS, CUSTOM],
required: true required: true
}, },
status: { status: {

View File

@ -1,137 +0,0 @@
import { Document, Schema, Types, model } from "mongoose";
import { IPType } from "../ee/models";
import { ADMIN, CUSTOM, MEMBER, VIEWER } from "../variables";
export interface IServiceTokenV3TrustedIp {
ipAddress: string;
type: IPType;
prefix: number;
}
export interface IServiceTokenDataV3 extends Document {
_id: Types.ObjectId;
name: string;
workspace: Types.ObjectId;
user: Types.ObjectId;
publicKey: string;
isActive: boolean;
refreshTokenLastUsed?: Date;
accessTokenLastUsed?: Date;
refreshTokenUsageCount: number;
accessTokenUsageCount: number;
tokenVersion: number;
isRefreshTokenRotationEnabled: boolean;
expiresAt?: Date;
accessTokenTTL: number;
role: "admin" | "member" | "viewer" | "custom";
customRole: Types.ObjectId;
trustedIps: Array<IServiceTokenV3TrustedIp>;
}
const serviceTokenDataV3Schema = new Schema(
{
name: {
type: String,
required: true
},
workspace: {
type: Schema.Types.ObjectId,
ref: "Workspace",
required: true
},
user: {
type: Schema.Types.ObjectId,
ref: "User",
required: true
},
publicKey: {
type: String,
required: true
},
isActive: {
type: Boolean,
default: true,
required: true
},
refreshTokenLastUsed: {
type: Date,
required: false
},
accessTokenLastUsed: {
type: Date,
required: false
},
refreshTokenUsageCount: {
type: Number,
default: 0,
required: true
},
accessTokenUsageCount: {
type: Number,
default: 0,
required: true
},
tokenVersion: {
type: Number,
default: 1,
required: true
},
isRefreshTokenRotationEnabled: {
type: Boolean,
default: false,
required: true
},
expiresAt: { // consider revising field name
type: Date,
required: false,
// expires: 0
},
accessTokenTTL: { // seconds
type: Number,
default: 7200,
required: true
},
role: {
type: String,
enum: [ADMIN, MEMBER, VIEWER, CUSTOM],
required: true
},
customRole: {
type: Schema.Types.ObjectId,
ref: "Role"
},
trustedIps: {
type: [
{
ipAddress: {
type: String,
required: true
},
type: {
type: String,
enum: [
IPType.IPV4,
IPType.IPV6
],
required: true
},
prefix: {
type: Number,
required: false
}
}
],
default: [{
ipAddress: "0.0.0.0",
type: IPType.IPV4.toString(),
prefix: 0
}],
required: true
}
},
{
timestamps: true
}
);
export const ServiceTokenDataV3 = model<IServiceTokenDataV3>("ServiceTokenDataV3", serviceTokenDataV3Schema);

View File

@ -1,43 +0,0 @@
import { Document, Schema, Types, model } from "mongoose";
export interface IServiceTokenDataV3Key extends Document {
_id: Types.ObjectId;
encryptedKey: string;
nonce: string;
sender: Types.ObjectId;
serviceTokenData: Types.ObjectId;
workspace: Types.ObjectId;
}
const serviceTokenDataV3KeySchema = new Schema(
{
encryptedKey: {
type: String,
required: true
},
nonce: {
type: String,
required: true
},
sender: {
type: Schema.Types.ObjectId,
ref: "User",
required: true
},
serviceTokenData: {
type: Schema.Types.ObjectId,
ref: "ServiceTokenDataV3",
required: true,
},
workspace: {
type: Schema.Types.ObjectId,
ref: "Workspace",
required: true,
}
},
{
timestamps: true
}
);
export const ServiceTokenDataV3Key = model<IServiceTokenDataV3Key>("ServiceTokenDataV3Key", serviceTokenDataV3KeySchema);

View File

@ -1,6 +1,7 @@
import signup from "./signup"; import signup from "./signup";
import bot from "./bot"; import bot from "./bot";
import auth from "./auth"; import auth from "./auth";
import universalAuth from "./universalAuth";
import user from "./user"; import user from "./user";
import userAction from "./userAction"; import userAction from "./userAction";
import organization from "./organization"; import organization from "./organization";
@ -23,6 +24,7 @@ import admin from "./admin";
export { export {
signup, signup,
auth, auth,
universalAuth,
bot, bot,
user, user,
userAction, userAction,

View File

@ -7,7 +7,7 @@ import { AuthMode } from "../../variables";
router.post( router.post(
"/", "/",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY] acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]
}), }),
secretImpsController.createSecretImp secretImpsController.createSecretImp
); );
@ -15,7 +15,7 @@ router.post(
router.put( router.put(
"/:id", "/:id",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY] acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]
}), }),
secretImpsController.updateSecretImport secretImpsController.updateSecretImport
); );
@ -23,7 +23,7 @@ router.put(
router.delete( router.delete(
"/:id", "/:id",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY] acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]
}), }),
secretImpsController.deleteSecretImport secretImpsController.deleteSecretImport
); );
@ -31,7 +31,7 @@ router.delete(
router.get( router.get(
"/", "/",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY] acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]
}), }),
secretImpsController.getSecretImports secretImpsController.getSecretImports
); );

View File

@ -12,7 +12,7 @@ import { AuthMode } from "../../variables";
router.post( router.post(
"/", "/",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY] acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]
}), }),
createFolder createFolder
); );
@ -20,7 +20,7 @@ router.post(
router.patch( router.patch(
"/:folderName", "/:folderName",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY] acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]
}), }),
updateFolderById updateFolderById
); );
@ -28,7 +28,7 @@ router.patch(
router.delete( router.delete(
"/:folderName", "/:folderName",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY] acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]
}), }),
deleteFolder deleteFolder
); );
@ -36,7 +36,7 @@ router.delete(
router.get( router.get(
"/", "/",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY] acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]
}), }),
getFolders getFolders
); );

View File

@ -0,0 +1,66 @@
import express from "express";
const router = express.Router();
import { requireAuth } from "../../middleware";
import { universalAuthController } from "../../controllers/v1";
import { AuthMode } from "../../variables";
router.post(
"/token/renew",
universalAuthController.renewAccessToken
);
router.post(
"/universal-auth/login",
universalAuthController.loginIdentityUniversalAuth
);
router.post(
"/universal-auth/identities/:identityId",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]
}),
universalAuthController.attachIdentityUniversalAuth
);
router.patch(
"/universal-auth/identities/:identityId",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]
}),
universalAuthController.updateIdentityUniversalAuth
);
router.get(
"/universal-auth/identities/:identityId",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]
}),
universalAuthController.getIdentityUniversalAuth
);
router.post(
"/universal-auth/identities/:identityId/client-secrets",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]
}),
universalAuthController.createUniversalAuthClientSecret
);
router.get(
"/universal-auth/identities/:identityId/client-secrets",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]
}),
universalAuthController.getUniversalAuthClientSecretsDetails
);
router.post(
"/universal-auth/identities/:identityId/client-secrets/:clientSecretId/revoke",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]
}),
universalAuthController.revokeUniversalAuthClientSecret
);
export default router;

View File

@ -7,7 +7,7 @@ import { AuthMode } from "../../variables";
router.post( router.post(
"/:workspaceId/environments", "/:workspaceId/environments",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]
}), }),
environmentController.createWorkspaceEnvironment environmentController.createWorkspaceEnvironment
); );
@ -15,7 +15,7 @@ router.post(
router.put( router.put(
"/:workspaceId/environments", "/:workspaceId/environments",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]
}), }),
environmentController.renameWorkspaceEnvironment environmentController.renameWorkspaceEnvironment
); );
@ -23,7 +23,7 @@ router.put(
router.patch( router.patch(
"/:workspaceId/environments", "/:workspaceId/environments",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]
}), }),
environmentController.reorderWorkspaceEnvironments environmentController.reorderWorkspaceEnvironments
); );
@ -31,7 +31,7 @@ router.patch(
router.delete( router.delete(
"/:workspaceId/environments", "/:workspaceId/environments",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]
}), }),
environmentController.deleteWorkspaceEnvironment environmentController.deleteWorkspaceEnvironment
); );

View File

@ -9,7 +9,7 @@ import { organizationsController } from "../../controllers/v2";
router.get( router.get(
"/:organizationId/memberships", "/:organizationId/memberships",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]
}), }),
organizationsController.getOrganizationMemberships organizationsController.getOrganizationMemberships
); );
@ -17,7 +17,7 @@ router.get(
router.patch( router.patch(
"/:organizationId/memberships/:membershipId", "/:organizationId/memberships/:membershipId",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]
}), }),
organizationsController.updateOrganizationMembership organizationsController.updateOrganizationMembership
); );
@ -25,7 +25,7 @@ router.patch(
router.delete( router.delete(
"/:organizationId/memberships/:membershipId", "/:organizationId/memberships/:membershipId",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]
}), }),
organizationsController.deleteOrganizationMembership organizationsController.deleteOrganizationMembership
); );
@ -33,7 +33,7 @@ router.delete(
router.get( router.get(
"/:organizationId/workspaces", "/:organizationId/workspaces",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]
}), }),
organizationsController.getOrganizationWorkspaces organizationsController.getOrganizationWorkspaces
); );
@ -54,4 +54,12 @@ router.delete(
organizationsController.deleteOrganizationById organizationsController.deleteOrganizationById
); );
router.get(
"/:organizationId/identity-memberships",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
organizationsController.getOrganizationIdentityMemberships
);
export default router; export default router;

View File

@ -6,7 +6,7 @@ import {
import { AuthMode } from "../../variables"; import { AuthMode } from "../../variables";
import { serviceTokenDataController } from "../../controllers/v2"; import { serviceTokenDataController } from "../../controllers/v2";
router.get( // TODO: deprecate (moving to ST V3) router.get( // TODO: deprecate (moving to identity)
"/", "/",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.SERVICE_TOKEN] acceptedAuthModes: [AuthMode.SERVICE_TOKEN]
@ -14,7 +14,7 @@ router.get( // TODO: deprecate (moving to ST V3)
serviceTokenDataController.getServiceTokenData serviceTokenDataController.getServiceTokenData
); );
router.post( // TODO: deprecate (moving to ST V3) router.post( // TODO: deprecate (moving to identity)
"/", "/",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT] acceptedAuthModes: [AuthMode.JWT]
@ -22,7 +22,7 @@ router.post( // TODO: deprecate (moving to ST V3)
serviceTokenDataController.createServiceTokenData serviceTokenDataController.createServiceTokenData
); );
router.delete( // TODO: deprecate (moving to ST V3) router.delete( // TODO: deprecate (moving to identity)
"/:serviceTokenDataId", "/:serviceTokenDataId",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT] acceptedAuthModes: [AuthMode.JWT]

View File

@ -62,7 +62,7 @@ router.get(
// new - TODO: rewire dashboard to this route // new - TODO: rewire dashboard to this route
"/:workspaceId/memberships", "/:workspaceId/memberships",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]
}), }),
workspaceController.getWorkspaceMemberships workspaceController.getWorkspaceMemberships
); );
@ -71,7 +71,7 @@ router.patch(
// TODO - rewire dashboard to this route // TODO - rewire dashboard to this route
"/:workspaceId/memberships/:membershipId", "/:workspaceId/memberships/:membershipId",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]
}), }),
workspaceController.updateWorkspaceMembership workspaceController.updateWorkspaceMembership
); );
@ -80,7 +80,7 @@ router.delete(
// TODO - rewire dashboard to this route // TODO - rewire dashboard to this route
"/:workspaceId/memberships/:membershipId", "/:workspaceId/memberships/:membershipId",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]
}), }),
workspaceController.deleteWorkspaceMembership workspaceController.deleteWorkspaceMembership
); );
@ -93,4 +93,37 @@ router.patch(
workspaceController.toggleAutoCapitalization workspaceController.toggleAutoCapitalization
); );
router.post(
"/:workspaceId/identity-memberships/:identityId",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]
}),
workspaceController.addIdentityToWorkspace
);
router.patch(
"/:workspaceId/identity-memberships/:identityId",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]
}),
workspaceController.updateIdentityWorkspaceRole
);
router.delete(
"/:workspaceId/identity-memberships/:identityId",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]
}),
workspaceController.deleteIdentityFromWorkspace
);
router.get(
"/:workspaceId/identity-memberships",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]
}),
workspaceController.getWorkspaceIdentityMemberships
);
export default router; export default router;

View File

@ -7,7 +7,7 @@ import { AuthMode } from "../../variables";
router.get( router.get(
"/raw", "/raw",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN] acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]
}), }),
secretsController.getSecretsRaw secretsController.getSecretsRaw
); );
@ -15,7 +15,7 @@ router.get(
router.get( router.get(
"/raw/:secretName", "/raw/:secretName",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN] acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]
}), }),
requireBlindIndicesEnabled({ requireBlindIndicesEnabled({
locationWorkspaceId: "query" locationWorkspaceId: "query"
@ -29,7 +29,7 @@ router.get(
router.post( router.post(
"/raw/:secretName", "/raw/:secretName",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN] acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]
}), }),
requireBlindIndicesEnabled({ requireBlindIndicesEnabled({
locationWorkspaceId: "body" locationWorkspaceId: "body"
@ -43,7 +43,7 @@ router.post(
router.patch( router.patch(
"/raw/:secretName", "/raw/:secretName",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN] acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]
}), }),
requireBlindIndicesEnabled({ requireBlindIndicesEnabled({
locationWorkspaceId: "body" locationWorkspaceId: "body"
@ -57,7 +57,7 @@ router.patch(
router.delete( router.delete(
"/raw/:secretName", "/raw/:secretName",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN] acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]
}), }),
requireBlindIndicesEnabled({ requireBlindIndicesEnabled({
locationWorkspaceId: "body" locationWorkspaceId: "body"
@ -71,7 +71,7 @@ router.delete(
router.get( router.get(
"/", "/",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN] acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN]
}), }),
requireBlindIndicesEnabled({ requireBlindIndicesEnabled({
locationWorkspaceId: "query" locationWorkspaceId: "query"
@ -116,7 +116,7 @@ router.delete(
router.post( router.post(
"/:secretName", "/:secretName",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN] acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN]
}), }),
requireBlindIndicesEnabled({ requireBlindIndicesEnabled({
locationWorkspaceId: "body" locationWorkspaceId: "body"
@ -127,7 +127,7 @@ router.post(
router.get( router.get(
"/:secretName", "/:secretName",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN] acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN]
}), }),
requireBlindIndicesEnabled({ requireBlindIndicesEnabled({
locationWorkspaceId: "query" locationWorkspaceId: "query"
@ -138,7 +138,7 @@ router.get(
router.patch( router.patch(
"/:secretName", "/:secretName",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN] acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN]
}), }),
requireBlindIndicesEnabled({ requireBlindIndicesEnabled({
locationWorkspaceId: "body" locationWorkspaceId: "body"
@ -149,7 +149,7 @@ router.patch(
router.delete( router.delete(
"/:secretName", "/:secretName",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN] acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN]
}), }),
requireBlindIndicesEnabled({ requireBlindIndicesEnabled({
locationWorkspaceId: "body" locationWorkspaceId: "body"

View File

@ -34,12 +34,4 @@ router.post(
// -- // --
router.get(
"/:workspaceId/service-token",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
workspacesController.getWorkspaceServiceTokenData
);
export default router; export default router;

View File

@ -0,0 +1,104 @@
import jwt from "jsonwebtoken";
import { IIdentity, IdentityAccessToken } from "../../../models";
import { getAuthSecret } from "../../../config";
import { AuthTokenType } from "../../../variables";
import { UnauthorizedRequestError } from "../../errors";
import { checkIPAgainstBlocklist } from "../../../utils/ip";
interface ValidateIdentityParams {
authTokenValue: string;
ipAddress: string;
}
export const validateIdentity = async ({
authTokenValue,
ipAddress
}: ValidateIdentityParams) => {
const decodedToken = <jwt.IdentityAccessTokenJwtPayload>(
jwt.verify(authTokenValue, await getAuthSecret())
);
if (decodedToken.authTokenType !== AuthTokenType.IDENTITY_ACCESS_TOKEN) throw UnauthorizedRequestError();
const identityAccessToken = await IdentityAccessToken
.findOne({
_id: decodedToken.identityAccessTokenId,
isAccessTokenRevoked: false
})
.populate<{ identity: IIdentity }>("identity");
if (!identityAccessToken || !identityAccessToken?.identity) throw UnauthorizedRequestError();
const {
accessTokenNumUsesLimit,
accessTokenNumUses,
accessTokenTTL,
accessTokenLastRenewedAt,
accessTokenMaxTTL,
createdAt: accessTokenCreatedAt
} = identityAccessToken;
checkIPAgainstBlocklist({
ipAddress,
trustedIps: identityAccessToken.accessTokenTrustedIps
});
// ttl check
if (accessTokenTTL > 0) {
const currentDate = new Date();
if (accessTokenLastRenewedAt) {
// access token has been renewed
const accessTokenRenewed = new Date(accessTokenLastRenewedAt);
const ttlInMilliseconds = accessTokenTTL * 1000;
const expirationDate = new Date(accessTokenRenewed.getTime() + ttlInMilliseconds);
if (currentDate > expirationDate) throw UnauthorizedRequestError({
message: "Failed to authenticate identity access token due to TTL expiration"
});
} else {
// access token has never been renewed
const accessTokenCreated = new Date(accessTokenCreatedAt);
const ttlInMilliseconds = accessTokenTTL * 1000;
const expirationDate = new Date(accessTokenCreated.getTime() + ttlInMilliseconds);
if (currentDate > expirationDate) throw UnauthorizedRequestError({
message: "Failed to authenticate identity access token due to TTL expiration"
});
}
}
// max ttl check
if (accessTokenMaxTTL > 0) {
const accessTokenCreated = new Date(accessTokenCreatedAt);
const ttlInMilliseconds = accessTokenMaxTTL * 1000;
const currentDate = new Date();
const expirationDate = new Date(accessTokenCreated.getTime() + ttlInMilliseconds);
if (currentDate > expirationDate) throw UnauthorizedRequestError({
message: "Failed to authenticate identity access token due to Max TTL expiration"
});
}
// num uses check
if (
accessTokenNumUsesLimit > 0
&& accessTokenNumUses === accessTokenNumUsesLimit
) {
throw UnauthorizedRequestError({
message: "Failed to authenticate MI access token due to access token number of uses limit reached"
});
}
await IdentityAccessToken.findByIdAndUpdate(
identityAccessToken._id,
{
accessTokenLastUsedAt: new Date(),
$inc: { accessTokenNumUses: 1 }
},
{
new: true
}
);
return identityAccessToken.identity;
}

View File

@ -2,4 +2,4 @@ export * from "./apiKey";
export * from "./apiKeyV2"; export * from "./apiKeyV2";
export * from "./jwt"; export * from "./jwt";
export * from "./serviceTokenV2"; export * from "./serviceTokenV2";
export * from "./serviceTokenV3"; export * from "./identity";

View File

@ -1,64 +0,0 @@
import jwt from "jsonwebtoken";
import { Types } from "mongoose";
import { ServiceTokenDataV3 } from "../../../models";
import { getAuthSecret } from "../../../config";
import { AuthTokenType } from "../../../variables";
import { UnauthorizedRequestError } from "../../errors";
interface ValidateServiceTokenV3Params {
authTokenValue: string;
}
export const validateServiceTokenV3 = async ({
authTokenValue
}: ValidateServiceTokenV3Params) => {
const decodedToken = <jwt.ServiceRefreshTokenJwtPayload>(
jwt.verify(authTokenValue, await getAuthSecret())
);
if (decodedToken.authTokenType !== AuthTokenType.SERVICE_ACCESS_TOKEN) throw UnauthorizedRequestError();
const serviceTokenData = await ServiceTokenDataV3.findOne({
_id: new Types.ObjectId(decodedToken.serviceTokenDataId),
isActive: true
});
if (!serviceTokenData) {
throw UnauthorizedRequestError({
message: "Failed to authenticate"
});
} else if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) {
// case: service token expired
await ServiceTokenDataV3.findByIdAndUpdate(
serviceTokenData._id,
{
isActive: false
},
{
new: true
}
);
throw UnauthorizedRequestError({
message: "Failed to authenticate",
});
} else if (decodedToken.tokenVersion !== serviceTokenData.tokenVersion) {
// TODO: raise alarm
throw UnauthorizedRequestError({
message: "Failed to authenticate",
});
}
await ServiceTokenDataV3.findByIdAndUpdate(
serviceTokenData._id,
{
accessTokenLastUsed: new Date(),
$inc: { accessTokenUsageCount: 1 }
},
{
new: true
}
);
return serviceTokenData;
}

View File

@ -1,7 +1,7 @@
import { AuthData } from "../../../interfaces/middleware"; import { AuthData } from "../../../interfaces/middleware";
import { import {
Identity,
ServiceTokenData, ServiceTokenData,
ServiceTokenDataV3,
User User
} from "../../../models"; } from "../../../models";
@ -19,7 +19,7 @@ import {
return { serviceTokenDataId: authData.authPayload._id }; return { serviceTokenDataId: authData.authPayload._id };
} }
if (authData.authPayload instanceof ServiceTokenDataV3) { if (authData.authPayload instanceof Identity) {
return { serviceTokenDataId: authData.authPayload._id }; return { serviceTokenDataId: authData.authPayload._id };
} }
}; };
@ -38,7 +38,7 @@ export const getAuthDataPayloadUserObj = (authData: AuthData) => {
return { user: authData.authPayload.user }; return { user: authData.authPayload.user };
} }
if (authData.authPayload instanceof ServiceTokenDataV3) { if (authData.authPayload instanceof Identity) {
return { user: authData.authPayload.user }; return {};
} }
} }

View File

@ -7,9 +7,9 @@ import { UnauthorizedRequestError } from "../../errors";
import { import {
validateAPIKey, validateAPIKey,
validateAPIKeyV2, validateAPIKeyV2,
validateIdentity,
validateJWT, validateJWT,
validateServiceTokenV2, validateServiceTokenV2
validateServiceTokenV3
} from "../authModeValidators"; } from "../authModeValidators";
import { getUserAgentType } from "../../posthog"; import { getUserAgentType } from "../../posthog";
@ -36,7 +36,7 @@ interface GetAuthDataParams {
* - SERVICE_TOKEN * - SERVICE_TOKEN
* - API_KEY * - API_KEY
* - JWT * - JWT
* - SERVICE_ACCESS_TOKEN (from ST V3) * - IDENTITY_ACCESS_TOKEN (from identity)
* - API_KEY_V2 * - API_KEY_V2
* @param {Object} params * @param {Object} params
* @param {Object.<string, (string|string[]|undefined)>} params.headers - The HTTP request headers, usually from Express's `req.headers`. * @param {Object.<string, (string|string[]|undefined)>} params.headers - The HTTP request headers, usually from Express's `req.headers`.
@ -77,8 +77,8 @@ export const extractAuthMode = async ({
return { authMode: AuthMode.JWT, authTokenValue }; return { authMode: AuthMode.JWT, authTokenValue };
case AuthTokenType.API_KEY: case AuthTokenType.API_KEY:
return { authMode: AuthMode.API_KEY_V2, authTokenValue }; return { authMode: AuthMode.API_KEY_V2, authTokenValue };
case AuthTokenType.SERVICE_ACCESS_TOKEN: case AuthTokenType.IDENTITY_ACCESS_TOKEN:
return { authMode: AuthMode.SERVICE_ACCESS_TOKEN, authTokenValue }; return { authMode: AuthMode.IDENTITY_ACCESS_TOKEN, authTokenValue };
default: default:
throw UnauthorizedRequestError({ throw UnauthorizedRequestError({
message: "Failed to authenticate unknown authentication method" message: "Failed to authenticate unknown authentication method"
@ -115,20 +115,21 @@ export const getAuthData = async ({
userAgentType userAgentType
} }
} }
case AuthMode.SERVICE_ACCESS_TOKEN: { case AuthMode.IDENTITY_ACCESS_TOKEN: {
const serviceTokenData = await validateServiceTokenV3({ const identity = await validateIdentity({
authTokenValue authTokenValue,
ipAddress
}); });
return { return {
actor: { actor: {
type: ActorType.SERVICE_V3, type: ActorType.IDENTITY,
metadata: { metadata: {
serviceId: serviceTokenData._id.toString(), identityId: identity._id.toString(),
name: serviceTokenData.name name: identity.name
} }
}, },
authPayload: serviceTokenData, authPayload: identity,
ipAddress, ipAddress,
userAgent, userAgent,
userAgentType userAgentType

View File

@ -53,9 +53,10 @@ export default class RequestError extends Error {
){ ){
super(message) super(message)
this._logLevel = logLevel || LogLevel.INFO this._logLevel = logLevel || LogLevel.INFO;
this._logName = LogLevel[this._logLevel]; this._logName = LogLevel[this._logLevel];
this.statusCode = statusCode this.statusCode = statusCode;
this.message = message;
this.type = type this.type = type
this.context = context || {} this.context = context || {}
this.extra = [] this.extra = []

View File

@ -84,6 +84,105 @@ export const ResetPasswordV1 = z.object({
}) })
}); });
export const RenewAccessTokenV1 = z.object({
body: z.object({
accessToken: z.string().trim(),
})
});
export const LoginUniversalAuthV1 = z.object({
body: z.object({
clientId: z.string().trim(),
clientSecret: z.string().trim()
})
});
export const AddUniversalAuthToIdentityV1 = z.object({
params: z.object({
identityId: z.string().trim()
}),
body: z.object({
clientSecretTrustedIps: z
.object({
ipAddress: z.string().trim(),
})
.array()
.min(1)
.default([{ ipAddress: "0.0.0.0/0" }]),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim(),
})
.array()
.min(1)
.default([{ ipAddress: "0.0.0.0/0" }]),
accessTokenTTL: z.number().int().min(1).refine(value => value !== 0, {
message: "accessTokenTTL must have a non zero number",
}).default(2592000),
accessTokenMaxTTL: z.number().int().refine(value => value !== 0, {
message: "accessTokenMaxTTL must have a non zero number",
}).default(2592000), // 30 days
accessTokenNumUsesLimit: z.number().int().min(0).default(0)
})
});
export const UpdateUniversalAuthToIdentityV1 = z.object({
params: z.object({
identityId: z.string()
}),
body: z.object({
clientSecretTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.optional(),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim(),
})
.array()
.min(1)
.optional(),
accessTokenTTL: z.number().int().min(0).optional(),
accessTokenNumUsesLimit: z.number().int().min(0).optional(),
accessTokenMaxTTL: z.number().int().refine(value => value !== 0, {
message: "accessTokenMaxTTL must have a non zero number",
}).optional(),
}),
});
export const GetUniversalAuthForIdentityV1 = z.object({
params: z.object({
identityId: z.string().trim()
})
});
export const CreateUniversalAuthClientSecretV1 = z.object({
params: z.object({
identityId: z.string()
}),
body: z.object({
description: z.string().trim().default(""),
numUsesLimit: z.number().min(0).default(0),
ttl: z.number().min(0).default(0),
}),
});
export const GetUniversalAuthClientSecretsV1 = z.object({
params: z.object({
identityId: z.string()
})
});
export const RevokeUniversalAuthClientSecretV1 = z.object({
params: z.object({
identityId: z.string(),
clientSecretId: z.string()
})
});
export const VerifyMfaTokenV2 = z.object({ export const VerifyMfaTokenV2 = z.object({
body: z.object({ body: z.object({
mfaToken: z.string().trim() mfaToken: z.string().trim()

View File

@ -0,0 +1,26 @@
import { z } from "zod";
import { NO_ACCESS } from "../variables";
export const CreateIdentityV1 = z.object({
body: z.object({
name: z.string().trim(),
organizationId: z.string().trim(),
role: z.string().trim().min(1).default(NO_ACCESS)
})
});
export const UpdateIdentityV1 = z.object({
params: z.object({
identityId: z.string()
}),
body: z.object({
name: z.string().trim().optional(),
role: z.string().trim().min(1).optional()
}),
});
export const DeleteIdentityV1 = z.object({
params: z.object({
identityId: z.string()
}),
});

View File

@ -8,5 +8,5 @@ export * from "./membershipOrg";
export * from "./organization"; export * from "./organization";
export * from "./secrets"; export * from "./secrets";
export * from "./serviceTokenData"; export * from "./serviceTokenData";
export * from "./serviceTokenDataV3"; export * from "./identities";
export * from "./apiKeyDataV3"; export * from "./apiKeyDataV3";

View File

@ -58,9 +58,9 @@ const validateClientForIntegrationAuth = async ({
throw UnauthorizedRequestError({ throw UnauthorizedRequestError({
message: "Failed service token authorization for integration authorization" message: "Failed service token authorization for integration authorization"
}); });
case ActorType.SERVICE_V3: case ActorType.IDENTITY:
throw UnauthorizedRequestError({ throw UnauthorizedRequestError({
message: "Failed service token authorization for integration authorization" message: "Failed identity authorization for integration authorization"
}); });
} }
}; };

View File

@ -46,9 +46,9 @@ export const validateClientForOrganization = async ({
throw UnauthorizedRequestError({ throw UnauthorizedRequestError({
message: "Failed service token authorization for organization" message: "Failed service token authorization for organization"
}); });
case ActorType.SERVICE_V3: case ActorType.IDENTITY:
throw UnauthorizedRequestError({ throw UnauthorizedRequestError({
message: "Failed service token authorization for organization" message: "Failed identity authorization for organization"
}); });
} }
}; };
@ -213,3 +213,11 @@ export const CreateOrgv2 = z.object({
export const DeleteOrgv2 = z.object({ export const DeleteOrgv2 = z.object({
params: z.object({ organizationId: z.string().trim() }) params: z.object({ organizationId: z.string().trim() })
}); });
export const GetOrgServiceMembersV2 = z.object({
params: z.object({ organizationId: z.string().trim() })
});
export const GetOrgIdentityMembershipsV2 = z.object({
params: z.object({ organizationId: z.string().trim() })
});

View File

@ -1,56 +0,0 @@
import { z } from "zod";
import { MEMBER } from "../variables";
export const RefreshTokenV3 = z.object({
body: z.object({
refresh_token: z.string().trim()
})
});
export const CreateServiceTokenV3 = z.object({
body: z.object({
name: z.string().trim(),
workspaceId: z.string().trim(),
publicKey: z.string().trim(),
role: z.string().trim().min(1).default(MEMBER),
trustedIps: z // TODO: provide default
.object({
ipAddress: z.string().trim(),
})
.array()
.min(1)
.default([{ ipAddress: "0.0.0.0/0" }]),
expiresIn: z.number().optional(),
accessTokenTTL: z.number().int().min(1),
encryptedKey: z.string().trim(),
nonce: z.string().trim(),
isRefreshTokenRotationEnabled: z.boolean().default(false)
})
});
export const UpdateServiceTokenV3 = z.object({
params: z.object({
serviceTokenDataId: z.string()
}),
body: z.object({
name: z.string().trim().optional(),
isActive: z.boolean().optional(),
role: z.string().trim().min(1).optional(),
trustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.optional(),
expiresIn: z.number().optional(),
accessTokenTTL: z.number().int().min(1).optional(),
isRefreshTokenRotationEnabled: z.boolean().optional()
}),
});
export const DeleteServiceTokenV3 = z.object({
params: z.object({
serviceTokenDataId: z.string()
}),
});

View File

@ -8,6 +8,7 @@ import { AuthData } from "../interfaces/middleware";
import { z } from "zod"; import { z } from "zod";
import { EventType, UserAgentType } from "../ee/models"; import { EventType, UserAgentType } from "../ee/models";
import { UnauthorizedRequestError } from "../utils/errors"; import { UnauthorizedRequestError } from "../utils/errors";
import { NO_ACCESS } from "../variables";
/** /**
* Validate authenticated clients for workspace with id [workspaceId] based * Validate authenticated clients for workspace with id [workspaceId] based
@ -59,9 +60,9 @@ export const validateClientForWorkspace = async ({
requiredPermissions requiredPermissions
}); });
return { membership, workspace }; return { membership, workspace };
case ActorType.SERVICE_V3: case ActorType.IDENTITY:
throw UnauthorizedRequestError({ throw UnauthorizedRequestError({
message: "Failed service token authorization for organization" message: "Failed identity authorization for organization"
}); });
} }
}; };
@ -279,6 +280,39 @@ export const ToggleAutoCapitalizationV2 = z.object({
}) })
}); });
export const AddIdentityToWorkspaceV2 = z.object({
params: z.object({
workspaceId: z.string().trim(),
identityId: z.string().trim()
}),
body: z.object({
role: z.string().trim().min(1).default(NO_ACCESS),
})
});
export const UpdateIdentityWorkspaceRoleV2 = z.object({
params: z.object({
workspaceId: z.string().trim(),
identityId: z.string().trim()
}),
body: z.object({
role: z.string().trim().min(1).default(NO_ACCESS),
})
});
export const DeleteIdentityFromWorkspaceV2 = z.object({
params: z.object({
workspaceId: z.string().trim(),
identityId: z.string().trim()
})
});
export const GetWorkspaceIdentityMembersV2 = z.object({
params: z.object({
workspaceId: z.string().trim()
}),
});
export const GetWorkspaceBlinkIndexStatusV3 = z.object({ export const GetWorkspaceBlinkIndexStatusV3 = z.object({
params: z.object({ params: z.object({
workspaceId: z.string().trim() workspaceId: z.string().trim()
@ -304,9 +338,3 @@ export const NameWorkspaceSecretsV3 = z.object({
.array() .array()
}) })
}); });
export const GetWorkspaceServiceTokenDataV3 = z.object({
params: z.object({
workspaceId: z.string().trim()
})
});

View File

@ -7,14 +7,13 @@ export enum AuthTokenType {
MFA_TOKEN = "mfaToken", // TODO: remove in favor of claim MFA_TOKEN = "mfaToken", // TODO: remove in favor of claim
PROVIDER_TOKEN = "providerToken", // TODO: remove in favor of claim PROVIDER_TOKEN = "providerToken", // TODO: remove in favor of claim
API_KEY = "apiKey", API_KEY = "apiKey",
SERVICE_ACCESS_TOKEN = "serviceAccessToken", IDENTITY_ACCESS_TOKEN = "identityAccessToken",
SERVICE_REFRESH_TOKEN = "serviceRefreshToken"
} }
export enum AuthMode { export enum AuthMode {
JWT = "jwt", JWT = "jwt",
SERVICE_TOKEN = "serviceToken", SERVICE_TOKEN = "serviceToken",
SERVICE_ACCESS_TOKEN = "serviceAccessToken", IDENTITY_ACCESS_TOKEN = "identityAccessToken",
API_KEY = "apiKey", API_KEY = "apiKey",
API_KEY_V2 = "apiKeyV2" API_KEY_V2 = "apiKeyV2"
} }

View File

@ -3,6 +3,7 @@ export const OWNER = "owner"; // depreciated
export const ADMIN = "admin"; export const ADMIN = "admin";
export const MEMBER = "member"; export const MEMBER = "member";
export const VIEWER = "viewer"; export const VIEWER = "viewer";
export const NO_ACCESS = "no-access";
export const CUSTOM = "custom"; export const CUSTOM = "custom";
// membership statuses // membership statuses

View File

@ -30,7 +30,7 @@ const generateOpenAPISpec = async () => {
type: "http", type: "http",
scheme: "bearer", scheme: "bearer",
bearerFormat: "JWT", bearerFormat: "JWT",
description: "A service token in Infisical" description: "An access token in Infisical"
}, },
apiKeyAuth: { apiKeyAuth: {
type: "apiKey", type: "apiKey",
@ -52,6 +52,41 @@ const generateOpenAPISpec = async () => {
updatedAt: "2023-01-13T14:16:12.210Z", updatedAt: "2023-01-13T14:16:12.210Z",
createdAt: "2023-01-13T14:16:12.210Z" createdAt: "2023-01-13T14:16:12.210Z"
}, },
Identity: {
_id: "",
name: "Machine 1",
authMethod: "universal-auth"
},
IdentityUniversalAuth: {
_id: "",
identity: "",
clientId: "...",
clientSecretTrustedIps: [{
ipAddress: "0.0.0.0",
type: "ipv4",
prefix: "0"
}],
accessTokenTTL: 7200,
accessTokenMaxTTL: 2592000,
accessTokenNumUsesLimit: 0,
accessTokenTrustedIps: [{
ipAddress: "0.0.0.0",
type: "ipv4",
prefix: "0"
}]
},
IdentityUniversalAuthClientSecretData: {
_id: "",
identityUniversalAuth: "",
isClientSecretRevoked: false,
description: "",
clientSecretPrefix: "abc",
clientSecretNumUses: 0,
clientSecretNumUsesLimit: 0,
clientSecretTTL: 0,
createdAt: "2023-01-13T14:16:12.210Z",
updatedAt: "2023-01-13T14:16:12.210Z"
},
Membership: { Membership: {
user: { user: {
_id: "", _id: "",
@ -79,6 +114,25 @@ const generateOpenAPISpec = async () => {
role: "owner", role: "owner",
status: "accepted" status: "accepted"
}, },
IdentityMembership: {
identity: {
_id: "",
name: "Machine 1",
authMethod: "universal-auth"
},
workspace: "",
role: "member"
},
IdentityMembershipOrg: {
identity: {
_id: "",
name: "Machine 1",
authMethod: "universal-auth"
},
organization: "",
role: "member",
status: "accepted"
},
Organization: { Organization: {
_id: "", _id: "",
name: "Acme Corp.", name: "Acme Corp.",

15
cli/agent-config.yaml Normal file
View File

@ -0,0 +1,15 @@
infisical:
address: "http://localhost:8080"
auth:
type: "universal-auth"
config:
client-id: "./client-id"
client-secret: "./client-secret"
remove_client_secret_on_read: false
sinks:
- type: "file"
config:
path: "access-token"
templates:
- source-path: my-dot-ev-secret-template
destination-path: my-dot-env.env

View File

@ -1,17 +0,0 @@
infisical:
address: "http://localhost:8080"
auth:
type: "token"
config:
token-path: "./role-id"
sinks:
- type: "file"
config:
path: "/Users/maidulislam/Desktop/test/infisical-token"
- type: "file"
config:
path: "access-token"
- type: "file"
config:
path: "maiduls-access-token"
templates:

View File

@ -21,8 +21,9 @@ require (
github.com/spf13/cobra v1.6.1 github.com/spf13/cobra v1.6.1
github.com/spf13/viper v1.8.1 github.com/spf13/viper v1.8.1
github.com/stretchr/testify v1.8.1 github.com/stretchr/testify v1.8.1
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d golang.org/x/crypto v0.14.0
golang.org/x/term v0.11.0 golang.org/x/term v0.13.0
gopkg.in/yaml.v2 v2.4.0
) )
require ( require (
@ -56,18 +57,17 @@ require (
github.com/subosito/gotenv v1.2.0 // indirect github.com/subosito/gotenv v1.2.0 // indirect
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
go.mongodb.org/mongo-driver v1.10.0 // indirect go.mongodb.org/mongo-driver v1.10.0 // indirect
golang.org/x/net v0.7.0 // indirect golang.org/x/net v0.17.0 // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.11.0 // indirect golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.7.0 // indirect golang.org/x/text v0.13.0 // indirect
gopkg.in/ini.v1 v1.62.0 // indirect gopkg.in/ini.v1 v1.62.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )
require ( require (
github.com/fatih/color v1.13.0 github.com/fatih/color v1.13.0
github.com/go-resty/resty/v2 v2.7.0 github.com/go-resty/resty/v2 v2.10.0
github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/jedib0t/go-pretty v4.3.0+incompatible github.com/jedib0t/go-pretty v4.3.0+incompatible
github.com/manifoldco/promptui v0.9.0 github.com/manifoldco/promptui v0.9.0

View File

@ -105,8 +105,8 @@ github.com/go-openapi/errors v0.20.2 h1:dxy7PGTqEh94zj2E3h1cUmQQWiM1+aeCROfAr02E
github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M=
github.com/go-openapi/strfmt v0.21.3 h1:xwhj5X6CjXEZZHMWy1zKJxvW9AfHC9pkyUjLvHtKG7o= github.com/go-openapi/strfmt v0.21.3 h1:xwhj5X6CjXEZZHMWy1zKJxvW9AfHC9pkyUjLvHtKG7o=
github.com/go-openapi/strfmt v0.21.3/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg= github.com/go-openapi/strfmt v0.21.3/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg=
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= github.com/go-resty/resty/v2 v2.10.0 h1:Qla4W/+TMmv0fOeeRqzEpXPLfTUnR5HZ1+lGs+CkiCo=
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= github.com/go-resty/resty/v2 v2.10.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@ -354,6 +354,7 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
@ -376,9 +377,11 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -414,6 +417,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -451,10 +456,12 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -477,8 +484,10 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -528,11 +537,18 @@ golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -542,11 +558,15 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@ -599,6 +619,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -425,24 +425,44 @@ func CallCreateServiceToken(httpClient *resty.Client, request CreateServiceToken
return createServiceTokenResponse, nil return createServiceTokenResponse, nil
} }
func CallServiceTokenV3Refresh(httpClient *resty.Client, request ServiceTokenV3RefreshTokenRequest) (ServiceTokenV3RefreshTokenResponse, error) { func CallUniversalAuthLogin(httpClient *resty.Client, request UniversalAuthLoginRequest) (UniversalAuthLoginResponse, error) {
var serviceTokenV3RefreshTokenResponse ServiceTokenV3RefreshTokenResponse var universalAuthLoginResponse UniversalAuthLoginResponse
response, err := httpClient. response, err := httpClient.
R(). R().
SetResult(&serviceTokenV3RefreshTokenResponse). SetResult(&universalAuthLoginResponse).
SetHeader("User-Agent", USER_AGENT). SetHeader("User-Agent", USER_AGENT).
SetBody(request). SetBody(request).
Post(fmt.Sprintf("%v/v3/service-token/me/token", config.INFISICAL_URL)) Post(fmt.Sprintf("%v/v1/auth/universal-auth/login/", config.INFISICAL_URL))
if err != nil { if err != nil {
return ServiceTokenV3RefreshTokenResponse{}, fmt.Errorf("CallServiceTokenV3Refresh: Unable to complete api request [err=%s]", err) return UniversalAuthLoginResponse{}, fmt.Errorf("CallUniversalAuthLogin: Unable to complete api request [err=%s]", err)
} }
if response.IsError() { if response.IsError() {
return ServiceTokenV3RefreshTokenResponse{}, fmt.Errorf("CallServiceTokenV3Refresh: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String()) return UniversalAuthLoginResponse{}, fmt.Errorf("CallUniversalAuthLogin: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String())
} }
return serviceTokenV3RefreshTokenResponse, nil return universalAuthLoginResponse, nil
}
func CallUniversalAuthRefreshAccessToken(httpClient *resty.Client, request UniversalAuthRefreshRequest) (UniversalAuthRefreshResponse, error) {
var universalAuthRefreshResponse UniversalAuthRefreshResponse
response, err := httpClient.
R().
SetResult(&universalAuthRefreshResponse).
SetHeader("User-Agent", USER_AGENT).
SetBody(request).
Post(fmt.Sprintf("%v/v1/auth/token/renew", config.INFISICAL_URL))
if err != nil {
return UniversalAuthRefreshResponse{}, fmt.Errorf("CallUniversalAuthRefreshAccessToken: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return UniversalAuthRefreshResponse{}, fmt.Errorf("CallUniversalAuthRefreshAccessToken: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String())
}
return universalAuthRefreshResponse, nil
} }
func CallGetRawSecretsV3(httpClient *resty.Client, request GetRawSecretsV3Request) (GetRawSecretsV3Response, error) { func CallGetRawSecretsV3(httpClient *resty.Client, request GetRawSecretsV3Request) (GetRawSecretsV3Response, error) {
@ -461,12 +481,12 @@ func CallGetRawSecretsV3(httpClient *resty.Client, request GetRawSecretsV3Reques
return GetRawSecretsV3Response{}, fmt.Errorf("CallGetRawSecretsV3: Unable to complete api request [err=%w]", err) return GetRawSecretsV3Response{}, fmt.Errorf("CallGetRawSecretsV3: Unable to complete api request [err=%w]", err)
} }
if response.IsError() && strings.Contains(response.String(), "Failed to find bot key") { if response.IsError() && strings.Contains(response.String(), "bot_not_found_error") {
return GetRawSecretsV3Response{}, fmt.Errorf("project with id %s is a legacy project type, please navigate to project settings and disable end to end encryption then try again", request.WorkspaceId) return GetRawSecretsV3Response{}, fmt.Errorf("project with id %s is a legacy project type, please navigate to project settings and disable end to end encryption then try again", request.WorkspaceId)
} }
if response.IsError() { if response.IsError() {
return GetRawSecretsV3Response{}, fmt.Errorf("CallGetRawSecretsV3: Unsuccessful response [%v %v] [status-code=%v]", response.Request.Method, response.Request.URL, response.StatusCode()) return GetRawSecretsV3Response{}, fmt.Errorf("CallGetRawSecretsV3: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String())
} }
return getRawSecretsV3Response, nil return getRawSecretsV3Response, nil

View File

@ -463,14 +463,27 @@ type CreateServiceTokenResponse struct {
ServiceTokenData ServiceTokenData `json:"serviceTokenData"` ServiceTokenData ServiceTokenData `json:"serviceTokenData"`
} }
type ServiceTokenV3RefreshTokenRequest struct { type UniversalAuthLoginRequest struct {
RefreshToken string `json:"refresh_token"` ClientSecret string `json:"clientSecret"`
ClientId string `json:"clientId"`
} }
type ServiceTokenV3RefreshTokenResponse struct {
RefreshToken string `json:"refresh_token"` type UniversalAuthLoginResponse struct {
AccessToken string `json:"access_token"` AccessToken string `json:"accessToken"`
ExpiresIn int `json:"expires_in"` AccessTokenTTL int `json:"expiresIn"`
TokenType string `json:"token_type"` TokenType string `json:"tokenType"`
AccessTokenMaxTTL int `json:"accessTokenMaxTTL"`
}
type UniversalAuthRefreshRequest struct {
AccessToken string `json:"accessToken"`
}
type UniversalAuthRefreshResponse struct {
AccessToken string `json:"accessToken"`
AccessTokenTTL int `json:"expiresIn"`
TokenType string `json:"tokenType"`
AccessTokenMaxTTL int `json:"accessTokenMaxTTL"`
} }
type GetRawSecretsV3Request struct { type GetRawSecretsV3Request struct {

View File

@ -5,12 +5,12 @@ package cmd
import ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"os/signal" "os/signal"
"strings" "strings"
"sync"
"syscall" "syscall"
"text/template" "text/template"
"time" "time"
@ -44,8 +44,10 @@ type AuthConfig struct {
Config interface{} `yaml:"config"` Config interface{} `yaml:"config"`
} }
type TokenAuthConfig struct { type UniversalAuth struct {
TokenPath string `yaml:"token-path"` ClientIDPath string `yaml:"client-id"`
ClientSecretPath string `yaml:"client-secret"`
RemoveClientSecretOnRead bool `yaml:"remove_client_secret_on_read"`
} }
type OAuthConfig struct { type OAuthConfig struct {
@ -149,11 +151,12 @@ func ParseAgentConfig(filePath string) (*Config, error) {
} }
switch rawConfig.Auth.Type { switch rawConfig.Auth.Type {
case "token": case "universal-auth":
var tokenConfig TokenAuthConfig var tokenConfig UniversalAuth
if err := yaml.Unmarshal(configBytes, &tokenConfig); err != nil { if err := yaml.Unmarshal(configBytes, &tokenConfig); err != nil {
return nil, err return nil, err
} }
config.Auth.Config = tokenConfig config.Auth.Config = tokenConfig
case "oauth": // aws, gcp, k8s service account, etc case "oauth": // aws, gcp, k8s service account, etc
var oauthConfig OAuthConfig var oauthConfig OAuthConfig
@ -199,59 +202,231 @@ func ProcessTemplate(templatePath string, data interface{}, accessToken string)
return &buf, nil return &buf, nil
} }
func refreshTokenAndProcessTemplate(refreshToken string, config *Config, errChan chan error) { type TokenManager struct {
for { accessToken string
accessTokenTTL time.Duration
accessTokenMaxTTL time.Duration
accessTokenFetchedTime time.Time
accessTokenRefreshedTime time.Time
mutex sync.Mutex
filePaths []Sink // Store file paths if needed
templates []Template
clientIdPath string
clientSecretPath string
newAccessTokenNotificationChan chan bool
removeClientSecretOnRead bool
cachedClientSecret string
}
func NewTokenManager(fileDeposits []Sink, templates []Template, clientIdPath string, clientSecretPath string, newAccessTokenNotificationChan chan bool, removeClientSecretOnRead bool) *TokenManager {
return &TokenManager{filePaths: fileDeposits, templates: templates, clientIdPath: clientIdPath, clientSecretPath: clientSecretPath, newAccessTokenNotificationChan: newAccessTokenNotificationChan, removeClientSecretOnRead: removeClientSecretOnRead}
}
func (tm *TokenManager) SetToken(token string, accessTokenTTL time.Duration, accessTokenMaxTTL time.Duration) {
tm.mutex.Lock()
defer tm.mutex.Unlock()
tm.accessToken = token
tm.accessTokenTTL = accessTokenTTL
tm.accessTokenMaxTTL = accessTokenMaxTTL
tm.newAccessTokenNotificationChan <- true
}
func (tm *TokenManager) GetToken() string {
tm.mutex.Lock()
defer tm.mutex.Unlock()
return tm.accessToken
}
// Fetches a new access token using client credentials
func (tm *TokenManager) FetchNewAccessToken() error {
clientIDAsByte, err := ReadFile(tm.clientIdPath)
if err != nil {
return fmt.Errorf("unable to read client id from file path '%s' due to error: %v", tm.clientIdPath, err)
}
clientSecretAsByte, err := ReadFile(tm.clientSecretPath)
if err != nil {
if len(tm.cachedClientSecret) == 0 {
return fmt.Errorf("unable to read client secret from file and no cached client secret found: %v", err)
} else {
clientSecretAsByte = []byte(tm.cachedClientSecret)
}
}
// remove client secret after first read
if tm.removeClientSecretOnRead {
os.Remove(tm.clientSecretPath)
}
clientId := string(clientIDAsByte)
clientSecret := string(clientSecretAsByte)
// save as cache in memory
tm.cachedClientSecret = clientSecret
err, loginResponse := universalAuthLogin(clientId, clientSecret)
if err != nil {
return err
}
accessTokenTTL := time.Duration(loginResponse.AccessTokenTTL * int(time.Second))
accessTokenMaxTTL := time.Duration(loginResponse.AccessTokenMaxTTL * int(time.Second))
if accessTokenTTL <= time.Duration(5)*time.Second {
util.PrintErrorMessageAndExit("At this this, agent does not support refresh of tokens with 5 seconds or less ttl. Please increase access token ttl and try again")
}
tm.accessTokenFetchedTime = time.Now()
tm.SetToken(loginResponse.AccessToken, accessTokenTTL, accessTokenMaxTTL)
return nil
}
// Refreshes the existing access token
func (tm *TokenManager) RefreshAccessToken() error {
httpClient := resty.New() httpClient := resty.New()
httpClient.SetRetryCount(10000). httpClient.SetRetryCount(10000).
SetRetryMaxWaitTime(20 * time.Second). SetRetryMaxWaitTime(20 * time.Second).
SetRetryWaitTime(5 * time.Second) SetRetryWaitTime(5 * time.Second)
tokenResponse, err := api.CallServiceTokenV3Refresh(httpClient, api.ServiceTokenV3RefreshTokenRequest{RefreshToken: refreshToken}) accessToken := tm.GetToken()
response, err := api.CallUniversalAuthRefreshAccessToken(httpClient, api.UniversalAuthRefreshRequest{AccessToken: accessToken})
if err != nil { if err != nil {
errChan <- fmt.Errorf("unable to complete renewal because [%s]", err) return err
} }
for _, sinkFile := range config.Sinks { accessTokenTTL := time.Duration(response.AccessTokenTTL * int(time.Second))
if sinkFile.Type == "file" { accessTokenMaxTTL := time.Duration(response.AccessTokenMaxTTL * int(time.Second))
err = ioutil.WriteFile(sinkFile.Config.Path, []byte(tokenResponse.AccessToken), 0644) tm.accessTokenRefreshedTime = time.Now()
tm.SetToken(response.AccessToken, accessTokenTTL, accessTokenMaxTTL)
return nil
}
func (tm *TokenManager) ManageTokenLifecycle() {
for {
accessTokenMaxTTLExpiresInTime := tm.accessTokenFetchedTime.Add(tm.accessTokenMaxTTL - (5 * time.Second))
accessTokenRefreshedTime := tm.accessTokenRefreshedTime
if accessTokenRefreshedTime.IsZero() {
accessTokenRefreshedTime = tm.accessTokenFetchedTime
}
nextAccessTokenExpiresInTime := accessTokenRefreshedTime.Add(tm.accessTokenTTL - (5 * time.Second))
if tm.accessTokenFetchedTime.IsZero() && tm.accessTokenRefreshedTime.IsZero() {
// case: init login to get access token
log.Info().Msg("attempting to authenticate...")
err := tm.FetchNewAccessToken()
if err != nil { if err != nil {
errChan <- err log.Error().Msgf("unable to authenticate because %v. Will retry in 30 seconds", err)
return
// wait a bit before trying again
time.Sleep((30 * time.Second))
continue
}
} else if time.Now().After(accessTokenMaxTTLExpiresInTime) {
log.Info().Msgf("token has reached max ttl, attempting to re authenticate...")
err := tm.FetchNewAccessToken()
if err != nil {
log.Error().Msgf("unable to authenticate because %v. Will retry in 30 seconds", err)
// wait a bit before trying again
time.Sleep((30 * time.Second))
continue
} }
} else { } else {
errChan <- errors.New("unsupported sink type. Only 'file' type is supported") log.Info().Msgf("attempting to refresh existing token...")
return err := tm.RefreshAccessToken()
}
}
refreshToken = tokenResponse.RefreshToken
nextRefreshCycle := time.Duration(tokenResponse.ExpiresIn-5) * time.Second // when the next access refresh will happen
d, err := time.ParseDuration(nextRefreshCycle.String())
if err != nil { if err != nil {
errChan <- fmt.Errorf("unable to parse refresh time because %s", err) log.Error().Msgf("unable to refresh token because %v. Will retry in 30 seconds", err)
return
// wait a bit before trying again
time.Sleep((30 * time.Second))
continue
}
} }
log.Info().Msgf("token refreshed and saved to selected path; next cycle will occur in %s", d.String()) if accessTokenRefreshedTime.IsZero() {
accessTokenRefreshedTime = tm.accessTokenFetchedTime
} else {
accessTokenRefreshedTime = tm.accessTokenRefreshedTime
}
for _, secretTemplate := range config.Templates { nextAccessTokenExpiresInTime = accessTokenRefreshedTime.Add(tm.accessTokenTTL - (5 * time.Second))
processedTemplate, err := ProcessTemplate(secretTemplate.SourcePath, nil, tokenResponse.AccessToken) accessTokenMaxTTLExpiresInTime = tm.accessTokenFetchedTime.Add(tm.accessTokenMaxTTL - (5 * time.Second))
if nextAccessTokenExpiresInTime.After(accessTokenMaxTTLExpiresInTime) {
// case: Refreshed so close that the next refresh would occur beyond max ttl (this is because currently, token renew tries to add +access-token-ttl amount of time)
// example: access token ttl is 11 sec and max ttl is 30 sec. So it will start with 11 seconds, then 22 seconds but the next time you call refresh it would try to extend it to 33 but max ttl only allows 30, so the token will be valid until 30 before we need to reauth
time.Sleep(tm.accessTokenTTL - nextAccessTokenExpiresInTime.Sub(accessTokenMaxTTLExpiresInTime))
} else {
time.Sleep(tm.accessTokenTTL - (5 * time.Second))
}
}
}
func (tm *TokenManager) WriteTokenToFiles() {
token := tm.GetToken()
for _, sinkFile := range tm.filePaths {
if sinkFile.Type == "file" {
err := ioutil.WriteFile(sinkFile.Config.Path, []byte(token), 0644)
if err != nil { if err != nil {
errChan <- err log.Error().Msgf("unable to write file sink to path '%s' because %v", sinkFile.Config.Path, err)
return }
log.Info().Msgf("new access token saved to file at path '%s'", sinkFile.Config.Path)
} else {
log.Error().Msg("unsupported sink type. Only 'file' type is supported")
}
}
}
func (tm *TokenManager) FetchSecrets() {
log.Info().Msgf("template engine started...")
for {
token := tm.GetToken()
if token != "" {
for _, secretTemplate := range tm.templates {
processedTemplate, err := ProcessTemplate(secretTemplate.SourcePath, nil, token)
if err != nil {
log.Error().Msgf("template engine: unable to render secrets because %s. Will try again on next cycle", err)
continue
} }
if err := WriteBytesToFile(processedTemplate, secretTemplate.DestinationPath); err != nil { if err := WriteBytesToFile(processedTemplate, secretTemplate.DestinationPath); err != nil {
errChan <- err log.Error().Msgf("template engine: unable to write secrets to path because %s. Will try again on next cycle", err)
return
continue
} }
log.Info().Msgf("secret template at path %s has been rendered and saved to path %s", secretTemplate.SourcePath, secretTemplate.DestinationPath) log.Info().Msgf("template engine: secret template at path %s has been rendered and saved to path %s", secretTemplate.SourcePath, secretTemplate.DestinationPath)
} }
time.Sleep(nextRefreshCycle) // fetch new secrets every 5 minutes (TODO: add PubSub in the future )
time.Sleep(5 * time.Minute)
} }
}
}
func universalAuthLogin(clientId string, clientSecret string) (error, api.UniversalAuthLoginResponse) {
httpClient := resty.New()
httpClient.SetRetryCount(10000).
SetRetryMaxWaitTime(20 * time.Second).
SetRetryWaitTime(5 * time.Second)
tokenResponse, err := api.CallUniversalAuthLogin(httpClient, api.UniversalAuthLoginRequest{ClientId: clientId, ClientSecret: clientSecret})
if err != nil {
return err, api.UniversalAuthLoginResponse{}
}
return nil, tokenResponse
} }
// runCmd represents the run command // runCmd represents the run command
@ -260,7 +435,7 @@ var agentCmd = &cobra.Command{
infisical agent infisical agent
`, `,
Use: "agent", Use: "agent",
Short: "Used to launch a client daemon that streamlines authentication and secret retrieval processes in some environments", Short: "Used to launch a client daemon that streamlines authentication and secret retrieval processes in various environments",
DisableFlagsInUseLine: true, DisableFlagsInUseLine: true,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
@ -282,37 +457,32 @@ var agentCmd = &cobra.Command{
return return
} }
errChan := make(chan error) if agentConfig.Auth.Type != "universal-auth" {
sigChan := make(chan os.Signal, 1) util.PrintErrorMessageAndExit("Only auth type of 'universal-auth' is supported at this time")
}
configUniversalAuthType := agentConfig.Auth.Config.(UniversalAuth)
tokenRefreshNotifier := make(chan bool)
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
switch configAuthType := agentConfig.Auth.Config.(type) { filePaths := agentConfig.Sinks
case TokenAuthConfig: tm := NewTokenManager(filePaths, agentConfig.Templates, configUniversalAuthType.ClientIDPath, configUniversalAuthType.ClientSecretPath, tokenRefreshNotifier, configUniversalAuthType.RemoveClientSecretOnRead)
content, err := ReadFile(configAuthType.TokenPath)
if err != nil {
log.Error().Msgf("unable to read initial token from file path %s because %v", configAuthType.TokenPath, err)
return
}
refreshToken := string(content) go tm.ManageTokenLifecycle()
go refreshTokenAndProcessTemplate(refreshToken, agentConfig, errChan) go tm.FetchSecrets()
case OAuthConfig:
// future auth types
default:
log.Error().Msgf("unknown auth config type. Only 'file' type is supported")
return
}
for {
select { select {
case err := <-errChan: case <-tokenRefreshNotifier:
log.Fatal().Msgf("agent stopped due to error: %v", err) go tm.WriteTokenToFiles()
os.Exit(1)
case <-sigChan: case <-sigChan:
log.Info().Msg("agent is gracefully shutting...") log.Info().Msg("agent is gracefully shutting...")
// TODO: check if we are in the middle of writing files to disk
os.Exit(1) os.Exit(1)
} }
}
}, },
} }

View File

@ -204,7 +204,8 @@ func init() {
func executeSingleCommandWithEnvs(args []string, secretsCount int, env []string) error { func executeSingleCommandWithEnvs(args []string, secretsCount int, env []string) error {
command := args[0] command := args[0]
argsForCommand := args[1:] argsForCommand := args[1:]
color.Green("Injecting %v Infisical secrets into your application process", secretsCount)
log.Info().Msgf(color.GreenString("Injecting %v Infisical secrets into your application process", secretsCount))
cmd := exec.Command(command, argsForCommand...) cmd := exec.Command(command, argsForCommand...)
cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin
@ -232,7 +233,7 @@ func executeMultipleCommandWithEnvs(fullCommand string, secretsCount int, env []
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
cmd.Env = env cmd.Env = env
color.Green("Injecting %v Infisical secrets into your application process", secretsCount) log.Info().Msgf(color.GreenString("Injecting %v Infisical secrets into your application process", secretsCount))
log.Debug().Msgf("executing command: %s %s %s \n", shell[0], shell[1], fullCommand) log.Debug().Msgf("executing command: %s %s %s \n", shell[0], shell[1], fullCommand)
return execCmd(cmd) return execCmd(cmd)

View File

@ -11,7 +11,6 @@ import (
"os/exec" "os/exec"
"runtime" "runtime"
"strings" "strings"
"time"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
@ -21,16 +20,16 @@ func CheckForUpdate() {
if checkEnv := os.Getenv("INFISICAL_DISABLE_UPDATE_CHECK"); checkEnv != "" { if checkEnv := os.Getenv("INFISICAL_DISABLE_UPDATE_CHECK"); checkEnv != "" {
return return
} }
latestVersion, publishedDate, err := getLatestTag("Infisical", "infisical") latestVersion, _, err := getLatestTag("Infisical", "infisical")
if err != nil { if err != nil {
log.Debug().Err(err) log.Debug().Err(err)
// do nothing and continue // do nothing and continue
return return
} }
daysSinceRelease, _ := daysSinceDate(publishedDate) // daysSinceRelease, _ := daysSinceDate(publishedDate)
if latestVersion != CLI_VERSION && daysSinceRelease > 2 { if latestVersion != CLI_VERSION {
yellow := color.New(color.FgYellow).SprintFunc() yellow := color.New(color.FgYellow).SprintFunc()
blue := color.New(color.FgCyan).SprintFunc() blue := color.New(color.FgCyan).SprintFunc()
black := color.New(color.FgBlack).SprintFunc() black := color.New(color.FgBlack).SprintFunc()
@ -151,15 +150,15 @@ func IsRunningInDocker() bool {
return strings.Contains(string(cgroup), "docker") return strings.Contains(string(cgroup), "docker")
} }
func daysSinceDate(dateString string) (int, error) { // func daysSinceDate(dateString string) (int, error) {
layout := "2006-01-02T15:04:05Z" // layout := "2006-01-02T15:04:05Z"
parsedDate, err := time.Parse(layout, dateString) // parsedDate, err := time.Parse(layout, dateString)
if err != nil { // if err != nil {
return 0, err // return 0, err
} // }
currentTime := time.Now() // currentTime := time.Now()
difference := currentTime.Sub(parsedDate) // difference := currentTime.Sub(parsedDate)
days := int(difference.Hours() / 24) // days := int(difference.Hours() / 24)
return days, nil // return days, nil
} // }

View File

@ -1,4 +1,4 @@
--- ---
title: "Create" title: "Create"
openapi: "POST /api/v1/workspace/{workspaceId}/environments" openapi: "POST /api/v2/workspace/{workspaceId}/environments"
--- ---

View File

@ -0,0 +1,4 @@
---
title: "Create"
openapi: "POST /api/v1/identities/"
---

View File

@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/identities/{identityId}"
---

View File

@ -0,0 +1,4 @@
---
title: "Update"
openapi: "PATCH /api/v1/identities/{identityId}"
---

View File

@ -1,4 +1,4 @@
--- ---
title: "Delete Membership" title: "Delete User Membership"
openapi: "DELETE /api/v2/organizations/{organizationId}/memberships/{membershipId}" openapi: "DELETE /api/v2/organizations/{organizationId}/memberships/{membershipId}"
--- ---

View File

@ -0,0 +1,4 @@
---
title: "List Identity Memberships"
openapi: "GET /api/v2/organizations/{organizationId}/identity-memberships"
---

View File

@ -1,4 +1,4 @@
--- ---
title: "Get Memberships" title: "Get User Memberships"
openapi: "GET /api/v2/organizations/{organizationId}/memberships" openapi: "GET /api/v2/organizations/{organizationId}/memberships"
--- ---

View File

@ -1,4 +1,4 @@
--- ---
title: "Update Membership" title: "Update User Membership"
openapi: "PATCH /api/v2/organizations/{organizationId}/memberships/{membershipId}" openapi: "PATCH /api/v2/organizations/{organizationId}/memberships/{membershipId}"
--- ---

View File

@ -2,3 +2,9 @@
title: "Get Projects" title: "Get Projects"
openapi: "GET /api/v2/organizations/{organizationId}/workspaces" openapi: "GET /api/v2/organizations/{organizationId}/workspaces"
--- ---
<Warning>
This endpoint will be deprecated in the near future in Q1/Q2 2024.
We recommend switching to using [identities](/documentation/platform/identities/overview).
</Warning>

View File

@ -2,3 +2,9 @@
title: "Get" title: "Get"
openapi: "GET /api/v2/service-token/" openapi: "GET /api/v2/service-token/"
--- ---
<Warning>
This endpoint will be deprecated in the near future with the removal of service tokens in Q1/Q2 2024.
We recommend switching to using [identities](/documentation/platform/identities/overview) if your client supports it.
</Warning>

View File

@ -0,0 +1,4 @@
---
title: "Attach"
openapi: "POST /api/v1/auth/universal-auth/identities/{identityId}"
---

Some files were not shown because too many files have changed in this diff Show More