mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Checkpoint service account functionality, added UI and general backend structure
This commit is contained in:
@@ -295,6 +295,7 @@ return res.status(200).send({
|
||||
*/
|
||||
export const getOrganizationServiceAccounts = async (req: Request, res: Response) => {
|
||||
const { organizationId } = req.params;
|
||||
|
||||
const serviceAccounts = await ServiceAccount.find({
|
||||
organization: new Types.ObjectId(organizationId)
|
||||
});
|
||||
|
||||
@@ -1,34 +1,49 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { Types } from 'mongoose';
|
||||
import crypto from 'crypto';
|
||||
import bcrypt from 'bcrypt';
|
||||
import {
|
||||
ServiceAccount,
|
||||
ServiceAccountKey,
|
||||
ServiceAccountPermission
|
||||
ServiceAccountOrganizationPermissions,
|
||||
ServiceAccountWorkspacePermissions
|
||||
} from '../../models';
|
||||
import {
|
||||
validateCreateServiceAccountPermission
|
||||
} from '../../helpers/serviceAccount';
|
||||
import {
|
||||
CreateServiceAccountDto,
|
||||
AddServiceAccountPermissionDto
|
||||
CreateServiceAccountDto
|
||||
} from '../../interfaces/serviceAccounts/dto';
|
||||
import {
|
||||
PERMISSION_SA_WORKSPACE_SET,
|
||||
PERMISSION_SA_SET
|
||||
} from '../../variables';
|
||||
import { ServiceAccountKeyNotFoundError, ValidationError } from '../../utils/errors';
|
||||
import { BadRequestError, ServiceAccountNotFoundError } from '../../utils/errors';
|
||||
import { getSaltRounds } from '../../config';
|
||||
|
||||
/**
|
||||
* Return service account with id [serviceAccountId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const getServiceAccount = async (req: Request, res: Response) => {
|
||||
const { serviceAccountId } = req.params;
|
||||
|
||||
const serviceAccount = await ServiceAccount.findById(serviceAccountId);
|
||||
|
||||
if (!serviceAccount) {
|
||||
throw ServiceAccountNotFoundError({ message: 'Failed to find service account' });
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
serviceAccount
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new service account under organization with id [organizationId]
|
||||
* that has access to workspaces [workspaces]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
* @returns
|
||||
*/
|
||||
export const createServiceAccount = async (req: Request, res: Response) => {
|
||||
const {
|
||||
organizationId,
|
||||
name,
|
||||
organizationId,
|
||||
publicKey,
|
||||
expiresIn,
|
||||
}: CreateServiceAccountDto = req.body;
|
||||
@@ -38,15 +53,59 @@ export const createServiceAccount = async (req: Request, res: Response) => {
|
||||
expiresAt = new Date();
|
||||
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
|
||||
}
|
||||
|
||||
const secret = crypto.randomBytes(16).toString('base64');
|
||||
const secretHash = await bcrypt.hash(secret, getSaltRounds());
|
||||
|
||||
// create service account
|
||||
const serviceAccount = await new ServiceAccount({
|
||||
name,
|
||||
organization: new Types.ObjectId(organizationId),
|
||||
user: req.user,
|
||||
publicKey,
|
||||
expiresAt
|
||||
expiresAt,
|
||||
secretHash
|
||||
}).save();
|
||||
|
||||
const serviceAccountObj = serviceAccount.toObject();
|
||||
|
||||
delete serviceAccountObj.secretHash;
|
||||
|
||||
// provision default org-level permissions for service account
|
||||
const permissions = await new ServiceAccountOrganizationPermissions({
|
||||
serviceAccount: serviceAccount._id
|
||||
}).save();
|
||||
|
||||
const secretId = Buffer.from(serviceAccount._id.toString(), 'hex').toString('base64');
|
||||
|
||||
return res.status(200).send({
|
||||
serviceAccountAccessKey: `SA.${secretId}.${secret}`,
|
||||
serviceAccount: serviceAccountObj
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Change name of service account with id [serviceAccountId] to [name]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const changeServiceAccountName = async (req: Request, res: Response) => {
|
||||
const { serviceAccountId } = req.params;
|
||||
const { name } = req.body;
|
||||
|
||||
const serviceAccount = await ServiceAccount.findOneAndUpdate(
|
||||
{
|
||||
_id: new Types.ObjectId(serviceAccountId)
|
||||
},
|
||||
{
|
||||
name
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
serviceAccount
|
||||
});
|
||||
@@ -78,66 +137,111 @@ export const addServiceAccountKey = async (req: Request, res: Response) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a permission to service account with id [serviceAccountId]
|
||||
* @param req
|
||||
* @param res
|
||||
* Return organization-level permissions for service account with id [serviceAccountId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const addServiceAccountPermission = async (req: Request, res: Response) => {
|
||||
const {
|
||||
name,
|
||||
workspaceId,
|
||||
environment
|
||||
}: AddServiceAccountPermissionDto = req.body;
|
||||
export const getServiceAccountOrganizationPermissions = async (req: Request, res: Response) => {
|
||||
const { serviceAccountId } = req.params;
|
||||
|
||||
if (PERMISSION_SA_WORKSPACE_SET.has(name)) {
|
||||
// case: permission named [name] is workspace-related
|
||||
|
||||
// some such permissions require workspaceId and environment to be present.
|
||||
|
||||
if (!workspaceId || !environment) {
|
||||
throw ValidationError({
|
||||
message: 'Failed validation that is workspace-related permission must specify a workspace and environment'
|
||||
});
|
||||
} else {
|
||||
const serviceAccountKey = await ServiceAccountKey.findOne({
|
||||
serviceAccount: req.serviceAccount._id,
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
if (!serviceAccountKey) throw ServiceAccountKeyNotFoundError({ message: 'Failed to find service account key' });
|
||||
}
|
||||
}
|
||||
|
||||
const serviceAccountPermission = await new ServiceAccountPermission({
|
||||
serviceAccount: req.serviceAccount._id,
|
||||
name,
|
||||
workspace: workspaceId ? new Types.ObjectId(workspaceId) : undefined,
|
||||
environment
|
||||
const permissions = await ServiceAccountOrganizationPermissions.findOne({
|
||||
serviceAccount: new Types.ObjectId(serviceAccountId),
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
serviceAccountPermission
|
||||
permissions
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a permission from service account with id [serviceAccountId]
|
||||
* Return workspace-level permissions for service account with id [serviceAccountId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const deleteServiceAccountPermission = async (req: Request, res: Response) => {
|
||||
const { serviceAccountPermissionId } = req.params;
|
||||
|
||||
// user must either be an admin/owner of the organization or they must
|
||||
// have created the service account in the first place to be able to delete it
|
||||
|
||||
// TODO: how to delete just 1 permission?
|
||||
|
||||
|
||||
const serviceAccountPermission = await ServiceAccountPermission.findByIdAndDelete(serviceAccountPermissionId);
|
||||
export const getServiceAccountWorkspacePermissions = async (req: Request, res: Response) => {
|
||||
const permissions = await ServiceAccountWorkspacePermissions.find({
|
||||
serviceAccount: req.serviceAccount._id
|
||||
}).populate('workspace');
|
||||
|
||||
return res.status(200).send({
|
||||
serviceAccountPermission
|
||||
permissions
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add organization permissions to service account with id [serviceAccountId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const addServiceAccountOrganizationPermission = async (req: Request, res: Response) => {
|
||||
const permissions = ServiceAccountOrganizationPermissions.findOne({
|
||||
serviceAccount: req.serviceAccount._id
|
||||
});
|
||||
|
||||
// TODO
|
||||
|
||||
return res.status(200).send({
|
||||
permissions
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a workspace permissions to service account with id [serviceAccountId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const addServiceAccountWorkspacePermission = async (req: Request, res: Response) => {
|
||||
const { serviceAccountId } = req.params;
|
||||
const {
|
||||
environment,
|
||||
workspaceId,
|
||||
canRead = false,
|
||||
canWrite = false,
|
||||
canUpdate = false,
|
||||
canDelete = false
|
||||
} = req.body;
|
||||
|
||||
if (!req.membership.workspace.environments.some((e: { name: string; slug: string }) => e.slug === environment)) {
|
||||
return res.status(400).send({
|
||||
message: 'Failed to validate workspace environment'
|
||||
});
|
||||
}
|
||||
|
||||
const existingPermission = await ServiceAccountWorkspacePermissions.findOne({
|
||||
serviceAccount: new Types.ObjectId(serviceAccountId),
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment
|
||||
});
|
||||
|
||||
if (existingPermission) throw BadRequestError({ message: 'Failed to add workspace permission to service account due to already-existing ' });
|
||||
|
||||
const permissions = await new ServiceAccountWorkspacePermissions({
|
||||
serviceAccount: new Types.ObjectId(serviceAccountId),
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
canRead,
|
||||
canWrite,
|
||||
canUpdate,
|
||||
canDelete
|
||||
}).save();
|
||||
|
||||
return res.status(200).send({
|
||||
permissions
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete workspace permissions from service account with id [serviceAccountId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const deleteServiceAccountWorkspacePermission = async (req: Request, res: Response) => {
|
||||
const { serviceAccountWorkspacePermissionsId } = req.params;
|
||||
|
||||
const permissions = await ServiceAccountWorkspacePermissions.findByIdAndDelete(serviceAccountWorkspacePermissionsId);
|
||||
|
||||
return res.status(200).send({
|
||||
permissions
|
||||
});
|
||||
}
|
||||
|
||||
@@ -156,10 +260,14 @@ export const deleteServiceAccount = async (req: Request, res: Response) => {
|
||||
// case: service account with id [serviceAccountId] was deleted
|
||||
|
||||
await ServiceAccountKey.deleteMany({
|
||||
serviceAccount: serviceAccount?._id
|
||||
serviceAccount: serviceAccount._id
|
||||
});
|
||||
|
||||
await ServiceAccountPermission.deleteMany({
|
||||
await ServiceAccountOrganizationPermissions.deleteMany({
|
||||
serviceAccount: new Types.ObjectId(serviceAccountId)
|
||||
});
|
||||
|
||||
await ServiceAccountWorkspacePermissions.deleteMany({
|
||||
serviceAccount: new Types.ObjectId(serviceAccountId)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
import { Types } from 'mongoose';
|
||||
import {
|
||||
Workspace,
|
||||
ServiceAccount,
|
||||
ServiceAccountKey
|
||||
} from '../models';
|
||||
import {
|
||||
WorkspaceNotFoundError,
|
||||
ServiceAccountNotFoundError,
|
||||
ServiceAccountKeyNotFoundError
|
||||
} from '../utils/errors';
|
||||
import {
|
||||
PERMISSION_SA_WORKSPACE_READ,
|
||||
PERMISSION_SA_WORKSPACE_WRITE,
|
||||
PERMISSION_SA_SET
|
||||
} from '../variables';
|
||||
|
||||
/**
|
||||
* Validate that user with id [userId] can provision the permission
|
||||
* named [name] for a service account with id [serviceAccountId] and
|
||||
* optionally workspace with id [workspaceId] and environment [environment]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.name - name of permission to create
|
||||
* @param {Types.ObjectId} userId - id of user creating the permission
|
||||
* @param {Types.ObjectId} serviceAccountId - id of service account that permission will be bound to
|
||||
* @param {Types.ObjectId} workspaceId - id of workspace that permission concerns
|
||||
* @param {Types.ObjectId} workspaceId - id of service account that permission will be bound to
|
||||
*/
|
||||
const validateCreateServiceAccountPermission = async ({
|
||||
name,
|
||||
userId,
|
||||
serviceAccountId,
|
||||
workspaceId,
|
||||
environment
|
||||
}: {
|
||||
name: string;
|
||||
userId: Types.ObjectId;
|
||||
serviceAccountId: Types.ObjectId,
|
||||
workspaceId?: Types.ObjectId;
|
||||
environment?: string;
|
||||
}) => {
|
||||
|
||||
// TODO: as we upgrade user permissions to be more global, then we should take into account
|
||||
// the user's permissions as it concerns to being able to interact with service accounts
|
||||
|
||||
if (!PERMISSION_SA_SET.has(name)) throw new Error(`${name} is not a valid permission name`);
|
||||
|
||||
if ([
|
||||
PERMISSION_SA_WORKSPACE_READ,
|
||||
PERMISSION_SA_WORKSPACE_WRITE
|
||||
].includes(name)) {
|
||||
if (workspaceId && environment) {
|
||||
// case: either workspace id [workspaceId] or environment name [environment] is being passed in
|
||||
// (i.e. validating a service account permission concerning a workspace and/or environment)
|
||||
const workspace = await Workspace.findById(workspaceId);
|
||||
|
||||
if (!workspace) {
|
||||
// case: workspace does not exist
|
||||
throw WorkspaceNotFoundError({ message: 'Failed to locate workspace' });
|
||||
}
|
||||
|
||||
if (!workspace.environments.some((env) => env.slug === environment)) {
|
||||
// case: environment name [environment] is not a valid environment slug in workspace
|
||||
throw Error('Failed to locate environment in workspace');
|
||||
}
|
||||
|
||||
const serviceAccount = await ServiceAccount.findById(serviceAccountId);
|
||||
if (!serviceAccount) {
|
||||
// case: service account does not exist
|
||||
throw ServiceAccountNotFoundError({ message: 'Failed to locate service account' });
|
||||
}
|
||||
|
||||
const serviceAccountKey = await ServiceAccountKey.findOne({
|
||||
serviceAccount: serviceAccount._id,
|
||||
workspace: workspaceId
|
||||
});
|
||||
|
||||
if (!serviceAccountKey) {
|
||||
// case: service account key does not exist
|
||||
throw ServiceAccountKeyNotFoundError({ message: 'Failed to locate service account key' });
|
||||
}
|
||||
} else {
|
||||
throw new Error('Failed to validate workspace and environment for workspace-related permission');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const validateDeleteServiceAccountPermission = async ({
|
||||
userId,
|
||||
serviceAccountId,
|
||||
name,
|
||||
workspaceId,
|
||||
environment
|
||||
}: {
|
||||
userId: Types.ObjectId;
|
||||
serviceAccountId: Types.ObjectId;
|
||||
name: string;
|
||||
workspaceId: Types.ObjectId;
|
||||
environment: string;
|
||||
}) => {
|
||||
// does the user have the authority to delete the permission?
|
||||
// does the service account permission exist?
|
||||
|
||||
|
||||
}
|
||||
|
||||
export {
|
||||
validateCreateServiceAccountPermission
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import requireIntegrationAuthorizationAuth from './requireIntegrationAuthorizati
|
||||
import requireServiceTokenAuth from './requireServiceTokenAuth';
|
||||
import requireServiceTokenDataAuth from './requireServiceTokenDataAuth';
|
||||
import requireServiceAccountAuth from './requireServiceAccountAuth';
|
||||
import requireServiceAccountWorkspacePermissionsAuth from './requireServiceAccountWorkspacePermissionsAuth';
|
||||
import requireSecretAuth from './requireSecretAuth';
|
||||
import requireSecretsAuth from './requireSecretsAuth';
|
||||
import validateRequest from './validateRequest';
|
||||
@@ -29,6 +30,7 @@ export {
|
||||
requireServiceTokenAuth,
|
||||
requireServiceTokenDataAuth,
|
||||
requireServiceAccountAuth,
|
||||
requireServiceAccountWorkspacePermissionsAuth,
|
||||
requireSecretAuth,
|
||||
requireSecretsAuth,
|
||||
validateRequest
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { ServiceAccount, ServiceAccountWorkspacePermissions } from '../models';
|
||||
import {
|
||||
ServiceAccountNotFoundError
|
||||
} from '../utils/errors';
|
||||
import {
|
||||
validateMembershipOrg
|
||||
} from '../helpers/membershipOrg';
|
||||
|
||||
type req = 'params' | 'body' | 'query';
|
||||
|
||||
const requireServiceAccountWorkspacePermissionsAuth = ({
|
||||
acceptedRoles,
|
||||
acceptedStatuses,
|
||||
location = 'params'
|
||||
}: {
|
||||
acceptedRoles: string[];
|
||||
acceptedStatuses: string[];
|
||||
location?: req;
|
||||
}) => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
const serviceAccountWorkspacePermissionsId = req[location].serviceAccountWorkspacePermissionsId;
|
||||
const serviceAccountWorkspacePermissions = await ServiceAccountWorkspacePermissions.findById(serviceAccountWorkspacePermissionsId);
|
||||
|
||||
if (!serviceAccountWorkspacePermissions) {
|
||||
return next(ServiceAccountNotFoundError({ message: 'Failed to locate Service Account workspace permission' }));
|
||||
}
|
||||
|
||||
const serviceAccount = await ServiceAccount.findById(serviceAccountWorkspacePermissions.serviceAccount);
|
||||
|
||||
if (!serviceAccount) {
|
||||
return next(ServiceAccountNotFoundError({ message: 'Failed to locate Service Account' }));
|
||||
}
|
||||
|
||||
if (serviceAccount.user.toString() !== req.user.id.toString()) {
|
||||
// case: creator of the service account is different from
|
||||
// the user on the request -> apply middleware role/status validation
|
||||
await validateMembershipOrg({
|
||||
userId: req.user._id,
|
||||
organizationId: serviceAccount.organization,
|
||||
acceptedRoles,
|
||||
acceptedStatuses
|
||||
});
|
||||
}
|
||||
|
||||
req.serviceAccount = serviceAccount;
|
||||
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
export default requireServiceAccountWorkspacePermissionsAuth;
|
||||
@@ -12,7 +12,8 @@ import Secret, { ISecret } from './secret';
|
||||
import ServiceToken, { IServiceToken } from './serviceToken';
|
||||
import ServiceAccount, { IServiceAccount } from './serviceAccount'; // new
|
||||
import ServiceAccountKey, { IServiceAccountKey } from './serviceAccountKey'; // new
|
||||
import ServiceAccountPermission, { IServiceAccountPermission } from './serviceAccountPermission';
|
||||
import ServiceAccountOrganizationPermissions, { IServiceAccountOrganizationPermissions } from './serviceAccountOrganizationPermission'; // new
|
||||
import ServiceAccountWorkspacePermissions, { IServiceAccountWorkspacePermissions } from './serviceAccountWorkspacePermissions'; // new
|
||||
import TokenData, { ITokenData } from './tokenData';
|
||||
import User, { IUser } from './user';
|
||||
import UserAction, { IUserAction } from './userAction';
|
||||
@@ -50,8 +51,10 @@ export {
|
||||
IServiceAccount,
|
||||
ServiceAccountKey,
|
||||
IServiceAccountKey,
|
||||
ServiceAccountPermission,
|
||||
IServiceAccountPermission,
|
||||
ServiceAccountOrganizationPermissions,
|
||||
IServiceAccountOrganizationPermissions,
|
||||
ServiceAccountWorkspacePermissions,
|
||||
IServiceAccountWorkspacePermissions,
|
||||
TokenData,
|
||||
ITokenData,
|
||||
User,
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface IServiceAccount extends Document {
|
||||
user: Types.ObjectId;
|
||||
publicKey: string;
|
||||
expiresAt: Date;
|
||||
secretHash: string;
|
||||
}
|
||||
|
||||
const serviceAccountSchema = new Schema<IServiceAccount>(
|
||||
@@ -31,6 +32,11 @@ const serviceAccountSchema = new Schema<IServiceAccount>(
|
||||
},
|
||||
expiresAt: {
|
||||
type: Date
|
||||
},
|
||||
secretHash: {
|
||||
type: String,
|
||||
required: true,
|
||||
select: false
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
28
backend/src/models/serviceAccountOrganizationPermission.ts
Normal file
28
backend/src/models/serviceAccountOrganizationPermission.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Schema, model, Types, Document } from 'mongoose';
|
||||
|
||||
export interface IServiceAccountOrganizationPermissions extends Document {
|
||||
_id: Types.ObjectId;
|
||||
serviceAccount: Types.ObjectId;
|
||||
canFoo: boolean;
|
||||
}
|
||||
|
||||
const serviceAccountOrganizationPermissionsSchema = new Schema<IServiceAccountOrganizationPermissions>(
|
||||
{
|
||||
serviceAccount: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'ServiceAccount',
|
||||
required: true
|
||||
},
|
||||
canFoo: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
const ServiceAccountOrganizationPermissions = model<IServiceAccountOrganizationPermissions>('ServiceAccountOrganizationPermissions', serviceAccountOrganizationPermissionsSchema);
|
||||
|
||||
export default ServiceAccountOrganizationPermissions;
|
||||
@@ -1,39 +0,0 @@
|
||||
import { Schema, model, Types, Document } from 'mongoose';
|
||||
|
||||
export interface IServiceAccountPermission extends Document {
|
||||
_id: Types.ObjectId;
|
||||
serviceAccount: Types.ObjectId;
|
||||
name: string;
|
||||
workspace?: Types.ObjectId;
|
||||
environment?: string;
|
||||
}
|
||||
|
||||
const serviceAccountPermissionSchema = new Schema<IServiceAccountPermission>(
|
||||
{
|
||||
serviceAccount: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'ServiceAccount',
|
||||
required: true
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Workspace',
|
||||
default: null
|
||||
},
|
||||
environment: {
|
||||
type: 'String',
|
||||
default: null
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
const ServiceAccountPermission = model<IServiceAccountPermission>('ServiceAccountPermission', serviceAccountPermissionSchema);
|
||||
|
||||
export default ServiceAccountPermission;
|
||||
54
backend/src/models/serviceAccountWorkspacePermissions.ts
Normal file
54
backend/src/models/serviceAccountWorkspacePermissions.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Schema, model, Types, Document } from 'mongoose';
|
||||
|
||||
export interface IServiceAccountWorkspacePermissions extends Document {
|
||||
_id: Types.ObjectId;
|
||||
serviceAccount: Types.ObjectId;
|
||||
workspace: Types.ObjectId;
|
||||
environment: string;
|
||||
canRead: boolean;
|
||||
canWrite: boolean;
|
||||
canUpdate: boolean;
|
||||
canDelete: boolean;
|
||||
}
|
||||
|
||||
const serviceAccountWorkspacePermissions = new Schema<IServiceAccountWorkspacePermissions>(
|
||||
{
|
||||
serviceAccount: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'ServiceAccount',
|
||||
required: true
|
||||
},
|
||||
workspace:{
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Workspace',
|
||||
required: true
|
||||
},
|
||||
environment: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
canRead: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
canWrite: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
canUpdate: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
canDelete: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
const ServiceAccountWorkspacePermissions = model<IServiceAccountWorkspacePermissions>('ServiceAccountWorkspacePermissions', serviceAccountWorkspacePermissions);
|
||||
|
||||
export default ServiceAccountWorkspacePermissions;
|
||||
@@ -1,27 +1,45 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import {
|
||||
requireAuth,
|
||||
requireOrganizationAuth,
|
||||
requireWorkspaceAuth,
|
||||
requireServiceAccountAuth,
|
||||
requireServiceAccountWorkspacePermissionsAuth,
|
||||
validateRequest
|
||||
} from '../../middleware';
|
||||
import { body } from 'express-validator';
|
||||
import { param, query, body } from 'express-validator';
|
||||
import {
|
||||
OWNER,
|
||||
ADMIN,
|
||||
MEMBER,
|
||||
ACCEPTED,
|
||||
PERMISSION_SA_SET
|
||||
ACCEPTED
|
||||
} from '../../variables';
|
||||
import { serviceAccountsController } from '../../controllers/v2';
|
||||
|
||||
router.get(
|
||||
'/:serviceAccountId',
|
||||
param('serviceAccountId').exists().isString().trim(),
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireServiceAccountAuth({
|
||||
acceptedRoles: [OWNER, ADMIN],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
}),
|
||||
serviceAccountsController.getServiceAccount
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
body('organizationId').exists().isString().trim(),
|
||||
body('name').exists().isString().trim(),
|
||||
body('publicKey').exists().isString().trim(),
|
||||
body('expiresIn'), // measured in ms
|
||||
body('expiresIn').isNumeric(), // measured in ms
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||
acceptedStatuses: [ACCEPTED],
|
||||
@@ -30,17 +48,119 @@ router.post(
|
||||
serviceAccountsController.createServiceAccount
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/serviceAccountId/:serviceAccountId/permissions',
|
||||
body('name').exists().isString().trim().custom((value) => PERMISSION_SA_SET.has(value)),
|
||||
body('workspaceId').optional().isMongoId(),
|
||||
body('environment').optional(),
|
||||
router.patch(
|
||||
'/:serviceAccountId/name',
|
||||
param('serviceAccountId').exists().isString().trim(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireServiceAccountAuth({
|
||||
acceptedRoles: [OWNER, ADMIN],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
}),
|
||||
serviceAccountsController.addServiceAccountPermission
|
||||
serviceAccountsController.changeServiceAccountName
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:serviceAccountId',
|
||||
param('serviceAccountId').exists().isString().trim(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireServiceAccountAuth({
|
||||
acceptedRoles: [OWNER, ADMIN],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
}),
|
||||
serviceAccountsController.deleteServiceAccount
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:serviceAccountId/permissions/organization',
|
||||
param('serviceAccountId').exists().isString().trim(),
|
||||
query('offset').exists(),
|
||||
query('limit').exists(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireServiceAccountAuth({
|
||||
acceptedRoles: [OWNER, ADMIN],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
}),
|
||||
serviceAccountsController.getServiceAccountOrganizationPermissions
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:serviceAccountId/permissions/workspace',
|
||||
param('serviceAccountId').exists().isString().trim(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireServiceAccountAuth({
|
||||
acceptedRoles: [OWNER, ADMIN],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
}),
|
||||
serviceAccountsController.getServiceAccountWorkspacePermissions
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/:serviceAccountId/permissions/organization',
|
||||
param('serviceAccountId').exists().isString().trim(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireServiceAccountAuth({
|
||||
acceptedRoles: [OWNER, ADMIN],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
}),
|
||||
serviceAccountsController.addServiceAccountOrganizationPermission
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/:serviceAccountId/permissions/workspace',
|
||||
param('serviceAccountId').exists().isString().trim(),
|
||||
body('workspaceId').exists().isString().notEmpty(),
|
||||
body('environment').exists().isString().notEmpty(),
|
||||
body('canRead').isBoolean().optional(),
|
||||
body('canWrite').isBoolean().optional(),
|
||||
body('canUpdate').isBoolean().optional(),
|
||||
body('canDelete').isBoolean().optional(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireServiceAccountAuth({
|
||||
acceptedRoles: [OWNER, ADMIN],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
location: 'body'
|
||||
}),
|
||||
serviceAccountsController.addServiceAccountWorkspacePermission
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:serviceAccountId/permissions/workspace/:serviceAccountWorkspacePermissionsId',
|
||||
param('serviceAccountId').exists().isString().trim(),
|
||||
param('serviceAccountWorkspacePermissionsId').exists().isString().trim(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireServiceAccountAuth({
|
||||
acceptedRoles: [OWNER, ADMIN],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
}),
|
||||
requireServiceAccountWorkspacePermissionsAuth({
|
||||
acceptedRoles: [OWNER, ADMIN],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
}),
|
||||
serviceAccountsController.deleteServiceAccountWorkspacePermission
|
||||
);
|
||||
|
||||
// router.post(
|
||||
@@ -55,28 +175,4 @@ router.post(
|
||||
// serviceAccountsController.addServiceAccountKey
|
||||
// );
|
||||
|
||||
router.delete(
|
||||
'/:serviceAccountId/key/:serviceAccountKeyId',
|
||||
requireServiceAccountAuth({
|
||||
acceptedRoles: [OWNER, ADMIN],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
}),
|
||||
async (req, res) => {
|
||||
// TODO: delete service account key id
|
||||
}
|
||||
);
|
||||
|
||||
// TODO: create service account permission
|
||||
// router.post(
|
||||
|
||||
// );
|
||||
|
||||
// TODO: delete service account permission
|
||||
|
||||
router.delete(
|
||||
'/:serviceAccountId/service-account-permission/:serviceAccountPermissionId',
|
||||
|
||||
)
|
||||
|
||||
|
||||
export default router;
|
||||
@@ -66,7 +66,7 @@ export const initSmtp = () => {
|
||||
const transporter = nodemailer.createTransport(mailOpts);
|
||||
transporter
|
||||
.verify()
|
||||
.then(() => {
|
||||
.then((err) => {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureMessage('SMTP - Successfully connected');
|
||||
})
|
||||
|
||||
@@ -63,12 +63,6 @@ import {
|
||||
TOKEN_EMAIL_ORG_INVITATION,
|
||||
TOKEN_EMAIL_PASSWORD_RESET
|
||||
} from './token';
|
||||
import {
|
||||
PERMISSION_SA_WORKSPACE_READ,
|
||||
PERMISSION_SA_WORKSPACE_WRITE,
|
||||
PERMISSION_SA_WORKSPACE_SET,
|
||||
PERMISSION_SA_SET
|
||||
} from './permissions';
|
||||
|
||||
export {
|
||||
OWNER,
|
||||
@@ -130,9 +124,5 @@ export {
|
||||
TOKEN_EMAIL_CONFIRMATION,
|
||||
TOKEN_EMAIL_MFA,
|
||||
TOKEN_EMAIL_ORG_INVITATION,
|
||||
TOKEN_EMAIL_PASSWORD_RESET,
|
||||
PERMISSION_SA_WORKSPACE_READ,
|
||||
PERMISSION_SA_WORKSPACE_WRITE,
|
||||
PERMISSION_SA_WORKSPACE_SET,
|
||||
PERMISSION_SA_SET
|
||||
TOKEN_EMAIL_PASSWORD_RESET
|
||||
};
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
const PERMISSION_SA_WORKSPACE_READ = 'read';
|
||||
const PERMISSION_SA_WORKSPACE_WRITE = 'write';
|
||||
|
||||
const PERMISSION_SA_WORKSPACE_SET = new Set([
|
||||
PERMISSION_SA_WORKSPACE_READ,
|
||||
PERMISSION_SA_WORKSPACE_WRITE
|
||||
]);
|
||||
|
||||
const PERMISSION_SA_SET = new Set([
|
||||
PERMISSION_SA_WORKSPACE_READ,
|
||||
PERMISSION_SA_WORKSPACE_WRITE
|
||||
]);
|
||||
|
||||
export {
|
||||
PERMISSION_SA_WORKSPACE_READ,
|
||||
PERMISSION_SA_WORKSPACE_WRITE,
|
||||
PERMISSION_SA_WORKSPACE_SET,
|
||||
PERMISSION_SA_SET
|
||||
}
|
||||
@@ -176,6 +176,7 @@ const Layout = ({ children }: LayoutProps) => {
|
||||
|
||||
useEffect(() => {
|
||||
// Put a user in a workspace if they're not in one yet
|
||||
|
||||
const putUserInWorkSpace = async () => {
|
||||
if (tempLocalStorage('orgData.id') === '') {
|
||||
const userOrgs = await getOrganizations();
|
||||
|
||||
@@ -3,21 +3,26 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import { useOrganization, useWorkspace } from '@app/context';
|
||||
|
||||
// TODO: make links clickable and clean up
|
||||
|
||||
/**
|
||||
* This is the component at the top of almost every page.
|
||||
* It shows how to navigate to a certain page.
|
||||
* It future these links should also be clickable and hoverable
|
||||
* @param obj
|
||||
* @param obj.pageName - Name of the page
|
||||
* @param obj.isProjectRelated - whether this page is related to project or now (determine if it's 2 or 3 navigation steps)
|
||||
* @param obj.isProjectRelated - whether or not this page is related to project (determine if it's 2 or 3 navigation steps)
|
||||
* @param obj.isOrganizationRelated - whether or not this page is related to organization (determine if it's 2 or 3 navigation steps)
|
||||
* @returns
|
||||
*/
|
||||
export default function NavHeader({
|
||||
pageName,
|
||||
isProjectRelated
|
||||
isProjectRelated,
|
||||
isOrganizationRelated
|
||||
}: {
|
||||
pageName: string;
|
||||
isProjectRelated?: boolean;
|
||||
isOrganizationRelated?: boolean;
|
||||
}): JSX.Element {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { currentOrg } = useOrganization();
|
||||
@@ -34,6 +39,12 @@ export default function NavHeader({
|
||||
<div className="text-sm font-semibold text-primary">{currentWorkspace?.name}</div>
|
||||
</>
|
||||
)}
|
||||
{isOrganizationRelated && (
|
||||
<>
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-sm text-gray-400" />
|
||||
<div className="text-sm font-semibold text-primary">Organization Settings</div>
|
||||
</>
|
||||
)}
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-sm text-gray-400" />
|
||||
<div className="text-sm text-gray-400">{pageName}</div>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,21 @@ import aes from './aes-256-gcm';
|
||||
const nacl = require('tweetnacl');
|
||||
nacl.util = require('tweetnacl-util');
|
||||
|
||||
/**
|
||||
* Return new base64, NaCl, public-private key pair.
|
||||
* @returns {Object} obj
|
||||
* @returns {String} obj.publicKey - base64, NaCl, public key
|
||||
* @returns {String} obj.privateKey - base64, NaCl, private key
|
||||
*/
|
||||
const generateKeyPair = () => {
|
||||
const pair = nacl.box.keyPair();
|
||||
|
||||
return ({
|
||||
publicKey: nacl.util.encodeBase64(pair.publicKey),
|
||||
privateKey: nacl.util.encodeBase64(pair.secretKey)
|
||||
});
|
||||
}
|
||||
|
||||
type EncryptAsymmetricProps = {
|
||||
plaintext: string;
|
||||
publicKey: string;
|
||||
@@ -189,5 +204,5 @@ export {
|
||||
decryptSymmetric,
|
||||
deriveArgonKey,
|
||||
encryptAssymmetric,
|
||||
encryptSymmetric
|
||||
};
|
||||
encryptSymmetric,
|
||||
generateKeyPair};
|
||||
|
||||
@@ -2,6 +2,7 @@ export * from './auth';
|
||||
export * from './incidentContacts';
|
||||
export * from './keys';
|
||||
export * from './organization';
|
||||
export * from './serviceAccounts';
|
||||
export * from './serviceTokens';
|
||||
export * from './subscriptions';
|
||||
export * from './tags';
|
||||
|
||||
9
frontend/src/hooks/api/serviceAccounts/index.tsx
Normal file
9
frontend/src/hooks/api/serviceAccounts/index.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
export {
|
||||
useCreateServiceAccount,
|
||||
useCreateServiceAccountProjectLevelPermissions,
|
||||
useDeleteServiceAccount,
|
||||
useDeleteServiceAccountProjectLevelPermissions,
|
||||
useGetServiceAccountById,
|
||||
useGetServiceAccountProjectLevelPermissions,
|
||||
useGetServiceAccounts,
|
||||
useRenameServiceAccount} from './queries';
|
||||
146
frontend/src/hooks/api/serviceAccounts/queries.tsx
Normal file
146
frontend/src/hooks/api/serviceAccounts/queries.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { apiRequest } from '@app/config/request';
|
||||
|
||||
import {
|
||||
CreateServiceAccountDTO,
|
||||
CreateServiceAccountRes,
|
||||
CreateServiceAccountWorkspacePermissionsDTO,
|
||||
DeleteServiceAccountRes,
|
||||
DeleteServiceAccountWorkspacePermissionsDTO,
|
||||
DeleteServiceAccountWorkspacePermissionsRes,
|
||||
RenameServiceAccountDTO,
|
||||
RenameServiceAccountRes,
|
||||
ServiceAccount,
|
||||
ServiceAccountWorkspacePermissions} from './types';
|
||||
|
||||
const serviceAccountKeys = {
|
||||
getServiceAccountById: (serviceAccountId: string) => [{ serviceAccountId }, 'service-account'] as const,
|
||||
getServiceAccounts: (organizationID: string) => [{ organizationID }, 'service-accounts'] as const,
|
||||
getServiceAccountProjectLevelPermissions: (serviceAccountId: string) => [{ serviceAccountId }, 'service-account-project-level-permissions'] as const
|
||||
}
|
||||
|
||||
const fetchServiceAccounts = async (organizationID: string) => {
|
||||
const { data } = await apiRequest.get<{ serviceAccounts: ServiceAccount[] }>(
|
||||
`/api/v2/organizations/${organizationID}/service-accounts`
|
||||
);
|
||||
|
||||
return data.serviceAccounts;
|
||||
}
|
||||
|
||||
const fetchServiceAccountById = async (serviceAccountId: string) => {
|
||||
const { data } = await apiRequest.get<{ serviceAccount: ServiceAccount }>(
|
||||
`/api/v2/service-accounts/${serviceAccountId}`
|
||||
);
|
||||
|
||||
return data.serviceAccount;
|
||||
}
|
||||
|
||||
export const useGetServiceAccounts = (organizationID: string) =>
|
||||
useQuery({
|
||||
queryKey: serviceAccountKeys.getServiceAccounts(organizationID),
|
||||
queryFn: () => fetchServiceAccounts(organizationID),
|
||||
enabled: Boolean(organizationID)
|
||||
});
|
||||
|
||||
export const useGetServiceAccountById = (serviceAccountId: string) => {
|
||||
return useQuery({
|
||||
queryKey: serviceAccountKeys.getServiceAccountById(serviceAccountId),
|
||||
queryFn: () => fetchServiceAccountById(serviceAccountId),
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
|
||||
export const useCreateServiceAccount = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<CreateServiceAccountRes, {}, CreateServiceAccountDTO>({
|
||||
mutationFn: async (body) => {
|
||||
const { data } = await apiRequest.post('/api/v2/service-accounts/', body);
|
||||
return data;
|
||||
},
|
||||
onSuccess: ({ serviceAccount }) => {
|
||||
queryClient.invalidateQueries(serviceAccountKeys.getServiceAccounts(serviceAccount.organization));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const useRenameServiceAccount = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<RenameServiceAccountRes, {}, RenameServiceAccountDTO>({
|
||||
mutationFn: async ({ serviceAccountId, name }) => {
|
||||
const { data: { serviceAccount } } = await apiRequest.patch(`/api/v2/service-accounts/${serviceAccountId}/name`, { name });
|
||||
return serviceAccount;
|
||||
},
|
||||
onSuccess: (serviceAccount) => {
|
||||
queryClient.invalidateQueries(serviceAccountKeys.getServiceAccountById(serviceAccount._id));
|
||||
queryClient.invalidateQueries(serviceAccountKeys.getServiceAccounts(serviceAccount.organization));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const useDeleteServiceAccount = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<DeleteServiceAccountRes, {}, string>({
|
||||
mutationFn: async (serviceAccountId) => {
|
||||
const { data: { serviceAccount } } = await apiRequest.delete(`/api/v2/service-accounts/${serviceAccountId}`);
|
||||
return serviceAccount;
|
||||
},
|
||||
onSuccess: ({ organization }) => {
|
||||
queryClient.invalidateQueries(serviceAccountKeys.getServiceAccounts(organization));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const fetchServiceAccountProjectLevelPermissions = async (serviceAccountId: string) => {
|
||||
const { data: { permissions } } = await apiRequest.get<{ permissions: ServiceAccountWorkspacePermissions[] }>(
|
||||
`/api/v2/service-accounts/${serviceAccountId}/permissions/workspace`
|
||||
);
|
||||
|
||||
console.log('fetchServiceAccountProjectLevelPermissions');
|
||||
console.log('prrr: ', permissions);
|
||||
|
||||
return permissions;
|
||||
}
|
||||
|
||||
export const useGetServiceAccountProjectLevelPermissions = (serviceAccountId: string) => {
|
||||
return useQuery({
|
||||
queryKey: serviceAccountKeys.getServiceAccountProjectLevelPermissions(serviceAccountId),
|
||||
queryFn: () => fetchServiceAccountProjectLevelPermissions(serviceAccountId),
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
|
||||
export const useCreateServiceAccountProjectLevelPermissions = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<CreateServiceAccountRes, {}, CreateServiceAccountWorkspacePermissionsDTO>({
|
||||
mutationFn: async (body) => {
|
||||
const { data: { permissions } } = await apiRequest.post(`/api/v2/service-accounts/${body.serviceAccountId}/permissions/workspace`, body);
|
||||
return permissions;
|
||||
},
|
||||
onSuccess: ({ serviceAccount }) => {
|
||||
queryClient.invalidateQueries(serviceAccountKeys.getServiceAccountProjectLevelPermissions(serviceAccount));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const useDeleteServiceAccountProjectLevelPermissions = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<DeleteServiceAccountWorkspacePermissionsRes, {}, DeleteServiceAccountWorkspacePermissionsDTO>({
|
||||
mutationFn: async ({ serviceAccountId, serviceAccountWorkspacePermissionsId }) => {
|
||||
const { data: { permissions } } = await apiRequest.delete(`/api/v2/service-accounts/${serviceAccountId}/permissions/workspace/${serviceAccountWorkspacePermissionsId}`);
|
||||
console.log('useDeleteServiceAccountProjectLevelPermissions');
|
||||
console.log('permissions: ', permissions);
|
||||
return permissions;
|
||||
},
|
||||
onSuccess: ({ serviceAccount }) => {
|
||||
console.log('onSuccess3: ', serviceAccount);
|
||||
queryClient.invalidateQueries(serviceAccountKeys.getServiceAccountProjectLevelPermissions(serviceAccount));
|
||||
// queryClient.invalidateQueries(serviceAccountKeys.getServiceAccounts(organization));
|
||||
}
|
||||
});
|
||||
}
|
||||
66
frontend/src/hooks/api/serviceAccounts/types.ts
Normal file
66
frontend/src/hooks/api/serviceAccounts/types.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
export type ServiceAccount = {
|
||||
_id: string;
|
||||
name: string;
|
||||
organization: string;
|
||||
user: string;
|
||||
publicKey: string;
|
||||
expiresAt: string;
|
||||
}
|
||||
|
||||
export type CreateServiceAccountDTO = {
|
||||
name: string;
|
||||
organizationId: string;
|
||||
publicKey: string;
|
||||
expiresIn: number;
|
||||
}
|
||||
|
||||
export type CreateServiceAccountRes = {
|
||||
serviceAccount: ServiceAccount;
|
||||
serviceAccountAccessKey: string;
|
||||
}
|
||||
|
||||
export type DeleteServiceAccountRes = {
|
||||
serviceAccount: ServiceAccount;
|
||||
}
|
||||
|
||||
export type RenameServiceAccountDTO = {
|
||||
serviceAccountId: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type RenameServiceAccountRes = {
|
||||
serviceAccount: ServiceAccount;
|
||||
}
|
||||
|
||||
export type ServiceAccountWorkspacePermissions = {
|
||||
serviceAccount: string;
|
||||
workspace: string;
|
||||
environment: string;
|
||||
canRead: boolean;
|
||||
canWrite: boolean;
|
||||
canUpdate: boolean;
|
||||
canDelete: boolean;
|
||||
}
|
||||
|
||||
export type CreateServiceAccountWorkspacePermissionsDTO = {
|
||||
serviceAccountId: string;
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
canRead: boolean;
|
||||
canWrite: boolean;
|
||||
canUpdate: boolean;
|
||||
canDelete: boolean;
|
||||
}
|
||||
|
||||
export type CreateServiceAccountWorkspacePermissionsRes = {
|
||||
permissions: ServiceAccountWorkspacePermissions
|
||||
}
|
||||
|
||||
export type DeleteServiceAccountWorkspacePermissionsDTO = {
|
||||
serviceAccountId: string;
|
||||
serviceAccountWorkspacePermissionsId: string;
|
||||
}
|
||||
|
||||
export type DeleteServiceAccountWorkspacePermissionsRes = {
|
||||
permissions: ServiceAccountWorkspacePermissions
|
||||
}
|
||||
@@ -93,8 +93,8 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
// Placing the localstorage as much as possible
|
||||
// Wait till tony integrates the azure and its launched
|
||||
useEffect(() => {
|
||||
|
||||
// Put a user in a workspace if they're not in one yet
|
||||
|
||||
const putUserInWorkSpace = async () => {
|
||||
if (tempLocalStorage('orgData.id') === '') {
|
||||
const userOrgs = await getOrganizations();
|
||||
@@ -114,9 +114,33 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
) {
|
||||
router.push('/noprojects');
|
||||
} else if (router.asPath !== '/noprojects') {
|
||||
const intendedWorkspaceId = router.asPath
|
||||
.split('/')
|
||||
[router.asPath.split('/').length - 1].split('?')[0];
|
||||
|
||||
// const pathSegments = router.asPath.split('/').filter(segment => segment.length > 0);
|
||||
|
||||
// let intendedWorkspaceId;
|
||||
// if (pathSegments.length >= 2 && pathSegments[0] === 'dashboard') {
|
||||
// intendedWorkspaceId = pathSegments[1];
|
||||
// } else if (pathSegments.length >= 3 && pathSegments[0] === 'settings') {
|
||||
// intendedWorkspaceId = pathSegments[2];
|
||||
// } else {
|
||||
// intendedWorkspaceId = router.asPath
|
||||
// .split('/')
|
||||
// [router.asPath.split('/').length - 1].split('?')[0];
|
||||
// }
|
||||
|
||||
const pathSegments = router.asPath.split('/').filter(segment => segment.length > 0);
|
||||
|
||||
let intendedWorkspaceId;
|
||||
if (pathSegments.length >= 2 && pathSegments[0] === 'dashboard') {
|
||||
[, intendedWorkspaceId] = pathSegments;
|
||||
} else if (pathSegments.length >= 3 && pathSegments[0] === 'settings') {
|
||||
[, , intendedWorkspaceId] = pathSegments;
|
||||
} else {
|
||||
const lastPathSegment = router.asPath.split('/').pop().split('?');
|
||||
[intendedWorkspaceId] = lastPathSegment;
|
||||
}
|
||||
|
||||
if (!intendedWorkspaceId) return;
|
||||
|
||||
if (!['callback', 'create', 'authorize'].includes(intendedWorkspaceId)) {
|
||||
localStorage.setItem('projectData.id', intendedWorkspaceId);
|
||||
@@ -192,7 +216,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
});
|
||||
|
||||
if (addMembers) {
|
||||
console.log('adding other users');
|
||||
// not using hooks because need at this point only
|
||||
const orgUsers = await fetchOrgUsers(currentOrg._id);
|
||||
orgUsers.forEach(({ status, user: orgUser }) => {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps';
|
||||
import { CreateServiceAccountPage } from '@app/views/Settings/CreateServiceAccountPage';
|
||||
|
||||
export default function ServiceAccountPage() {
|
||||
const router = useRouter();
|
||||
// const { orgId, serviceAccountId } = router.query;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Edit Service Account</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<div />
|
||||
<CreateServiceAccountPage />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
ServiceAccountPage.requireAuth = true;
|
||||
|
||||
export const getServerSidePros = getTranslatedServerSideProps([
|
||||
'settings',
|
||||
'settings-org',
|
||||
'section-incident',
|
||||
'section-members'
|
||||
]);
|
||||
20
frontend/src/pages/settings/service-account/[id].tsx
Normal file
20
frontend/src/pages/settings/service-account/[id].tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import Head from 'next/head';
|
||||
|
||||
export default function NewServiceAccountPage() {
|
||||
console.log('NewServiceAccountPage');
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
<title>Some title</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<div>
|
||||
Hello!
|
||||
</div>
|
||||
{/* <OrgSettingsPage /> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// NewServiceAccountPage.requireAuth = true;
|
||||
@@ -0,0 +1,43 @@
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
import NavHeader from '@app/components/navigation/NavHeader';
|
||||
|
||||
import { SAProjectLevelPermissionsTable } from './components/SAProjectLevelPermissionsTable';
|
||||
import { ServiceAccountNameChangeSection } from './components';
|
||||
|
||||
export const CreateServiceAccountPage = () => {
|
||||
const router = useRouter();
|
||||
const { serviceAccountId }: { serviceAccountId: string } = router.query;
|
||||
|
||||
return (
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<NavHeader
|
||||
pageName="Service Account"
|
||||
isOrganizationRelated
|
||||
/>
|
||||
<div className="my-8 ml-6 max-w-5xl">
|
||||
<p className="text-3xl font-semibold text-gray-200">Service Account</p>
|
||||
<p className="text-base font-normal text-gray-400">
|
||||
A service account represents a machine identity such as a VM or application client.
|
||||
</p>
|
||||
</div>
|
||||
<div className="max-w-8xl mx-6">
|
||||
{typeof serviceAccountId === 'string' && (
|
||||
<ServiceAccountNameChangeSection
|
||||
serviceAccountId={serviceAccountId}
|
||||
/>
|
||||
)}
|
||||
{/* <div className="rounded-md bg-white/5 p-6 mt-6">
|
||||
<p className="mb-4 text-xl font-semibold">Organization-Level Permissions</p>
|
||||
<SAProjectLevelPermissionsTable />
|
||||
</div> */}
|
||||
<div className="rounded-md bg-white/5 p-6 mt-6">
|
||||
<p className="mb-4 text-xl font-semibold">Project-Level Permissions</p>
|
||||
<SAProjectLevelPermissionsTable
|
||||
serviceAccountId={serviceAccountId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,394 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Controller,useForm } from 'react-hook-form';
|
||||
import {
|
||||
faKey,
|
||||
faMagnifyingGlass,
|
||||
faPlus,
|
||||
faTrash} from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import * as yup from 'yup';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
DeleteActionModal,
|
||||
EmptyState,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
Modal,
|
||||
ModalClose,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr} from '@app/components/v2';
|
||||
import { usePopUp } from '@app/hooks';
|
||||
import {
|
||||
useCreateServiceAccountProjectLevelPermissions,
|
||||
useDeleteServiceAccountProjectLevelPermissions,
|
||||
useGetServiceAccountProjectLevelPermissions,
|
||||
useGetUserWorkspaces} from '@app/hooks/api';
|
||||
|
||||
const createProjectLevelPermissionSchema = yup.object({
|
||||
workspace: yup.string().required().label('Workspace'),
|
||||
environment: yup.string().required().label('Environment'),
|
||||
permissions: yup.object().shape({
|
||||
canRead: yup.boolean().required(),
|
||||
canWrite: yup.boolean().required(),
|
||||
canUpdate: yup.boolean().required(),
|
||||
canDelete: yup.boolean().required(),
|
||||
}).defined().required()
|
||||
});
|
||||
|
||||
type CreateProjectLevelPermissionForm = yup.InferType<typeof createProjectLevelPermissionSchema>;
|
||||
|
||||
type Props = {
|
||||
serviceAccountId: string;
|
||||
}
|
||||
|
||||
export const SAProjectLevelPermissionsTable = ({
|
||||
serviceAccountId
|
||||
}: Props) => {
|
||||
const { data: userWorkspaces, isLoading: isUserWorkspacesLoading } = useGetUserWorkspaces();
|
||||
const [searchPermissions, setSearchPermissions] = useState('');
|
||||
const [defaultValues, setDefaultValues] = useState<CreateProjectLevelPermissionForm | undefined>(undefined);
|
||||
|
||||
const { data: permissions, isLoading: isPermissionsLoading } = useGetServiceAccountProjectLevelPermissions(serviceAccountId);
|
||||
|
||||
const createServiceAccountProjectLevelPermissions = useCreateServiceAccountProjectLevelPermissions();
|
||||
const deleteServiceAccountProjectLevelPermissions = useDeleteServiceAccountProjectLevelPermissions();
|
||||
|
||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||
'addProjectLevelPermissions',
|
||||
'removeProjectLevelPermissions',
|
||||
] as const);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<CreateProjectLevelPermissionForm>({ resolver: yupResolver(createProjectLevelPermissionSchema), defaultValues })
|
||||
|
||||
const onAddProjectLevelPermissions = async ({
|
||||
workspace,
|
||||
environment,
|
||||
permissions: { canRead, canWrite, canUpdate, canDelete }
|
||||
}: CreateProjectLevelPermissionForm) => {
|
||||
await createServiceAccountProjectLevelPermissions.mutateAsync({
|
||||
serviceAccountId,
|
||||
workspaceId: workspace,
|
||||
environment,
|
||||
canRead,
|
||||
canWrite,
|
||||
canUpdate,
|
||||
canDelete
|
||||
});
|
||||
handlePopUpClose('addProjectLevelPermissions');
|
||||
}
|
||||
|
||||
const onRemoveProjectLevelPermissions = async () => {
|
||||
const serviceAccountWorkspacePermissionsId = (popUp?.removeProjectLevelPermissions?.data as { _id: string })?._id;
|
||||
await deleteServiceAccountProjectLevelPermissions.mutateAsync({
|
||||
serviceAccountId,
|
||||
serviceAccountWorkspacePermissionsId
|
||||
});
|
||||
handlePopUpClose('removeProjectLevelPermissions');
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (userWorkspaces) {
|
||||
setDefaultValues({
|
||||
workspace: String(userWorkspaces?.[0]?._id),
|
||||
environment: String(userWorkspaces?.[0]?.environments?.[0]?.slug),
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: false,
|
||||
canUpdate: false,
|
||||
canDelete: false,
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [userWorkspaces]);
|
||||
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="mb-4 flex">
|
||||
<div className="mr-4 flex-1">
|
||||
<Input
|
||||
value={searchPermissions}
|
||||
onChange={(e) => setSearchPermissions(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search service account project-level permissions..."
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
handlePopUpOpen('addProjectLevelPermissions')
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
Add Permission
|
||||
</Button>
|
||||
</div>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Project</Th>
|
||||
<Th>Environment</Th>
|
||||
<Th>Read</Th>
|
||||
<Th>Write</Th>
|
||||
<Th>Update</Th>
|
||||
<Th>Delete</Th>
|
||||
<Th aria-label="actions" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isPermissionsLoading && <TableSkeleton columns={6} key="service-account-project-level-permissions" />}
|
||||
{!isPermissionsLoading && permissions && (
|
||||
permissions.map(({
|
||||
_id,
|
||||
workspace,
|
||||
environment,
|
||||
canRead,
|
||||
canWrite,
|
||||
canUpdate,
|
||||
canDelete
|
||||
}) => {
|
||||
const environmentName = (workspace.environments.find((env) => env.slug === environment))?.name;
|
||||
return (
|
||||
<Tr key={`service-account-project-level-permission-${_id}`} className="w-full">
|
||||
<Td>{workspace.name}</Td>
|
||||
<Td>{environmentName}</Td>
|
||||
<Td>
|
||||
<Checkbox
|
||||
id="isReadPermissionEnabled"
|
||||
isChecked={canRead}
|
||||
isDisabled
|
||||
/>
|
||||
</Td>
|
||||
<Td>
|
||||
<Checkbox
|
||||
id="isWritePermissionEnabled"
|
||||
isChecked={canWrite}
|
||||
isDisabled
|
||||
/>
|
||||
</Td>
|
||||
<Td>
|
||||
<Checkbox
|
||||
id="isUpdatePermissionEnabled"
|
||||
isChecked={canUpdate}
|
||||
isDisabled
|
||||
/>
|
||||
</Td>
|
||||
<Td>
|
||||
<Checkbox
|
||||
id="isDeletePermissionEnabled"
|
||||
isChecked={canDelete}
|
||||
/>
|
||||
</Td>
|
||||
<Td>
|
||||
<IconButton
|
||||
ariaLabel="delete"
|
||||
colorSchema="danger"
|
||||
onClick={() => handlePopUpOpen('removeProjectLevelPermissions', { _id })}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
{!isPermissionsLoading && permissions?.length === 0 && (
|
||||
<Tr>
|
||||
<Td colSpan={7} className="py-6 text-center text-bunker-400">
|
||||
<EmptyState title="No permissions found" icon={faKey} />
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Modal
|
||||
isOpen={popUp?.addProjectLevelPermissions?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle('addProjectLevelPermissions', isOpen);
|
||||
}}
|
||||
>
|
||||
<ModalContent
|
||||
title="Add a Project-Level Permission"
|
||||
subTitle="The service account will be granted scoped access to the specified project and environment"
|
||||
>
|
||||
<form onSubmit={handleSubmit(onAddProjectLevelPermissions)}>
|
||||
{!isUserWorkspacesLoading && userWorkspaces && (
|
||||
<>
|
||||
<Controller
|
||||
control={control}
|
||||
name="workspace"
|
||||
defaultValue={String(userWorkspaces?.[0]?._id)}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Project"
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full border border-mine-shaft-500"
|
||||
>
|
||||
{userWorkspaces && userWorkspaces.length > 0 ? (
|
||||
userWorkspaces.map((userWorkspace) => {
|
||||
return (
|
||||
<SelectItem value={userWorkspace._id} key={`project-${userWorkspace._id}`}>
|
||||
{userWorkspace.name}
|
||||
</SelectItem>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<SelectItem value="none" key="target-app-none">
|
||||
No projects found
|
||||
</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="environment"
|
||||
defaultValue={String(userWorkspaces?.[0]?.environments?.[0]?.slug)}
|
||||
render={({ field: { onChange, ...field } }) => {
|
||||
/* eslint-disable-next-line no-underscore-dangle */
|
||||
const environments = userWorkspaces?.find((userWorkspace) => userWorkspace._id === control?._formValues?.workspace)?.environments ?? [];
|
||||
return (
|
||||
<FormControl
|
||||
label="Environment"
|
||||
className="mt-4"
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full border border-mine-shaft-500"
|
||||
>
|
||||
{environments.length > 0 ? (
|
||||
environments.map((environment) => {
|
||||
return (
|
||||
<SelectItem value={environment.slug} key={`environment-${environment.slug}`}>
|
||||
{environment.name}
|
||||
</SelectItem>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<SelectItem value="none" key="target-app-none">
|
||||
No environments found
|
||||
</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name="permissions"
|
||||
defaultValue={{
|
||||
canRead: true,
|
||||
canWrite: false,
|
||||
canUpdate: false,
|
||||
canDelete: false
|
||||
}}
|
||||
render={({ field: { onChange, value }, fieldState: { error }}) => {
|
||||
const options = [
|
||||
{
|
||||
label: 'Read (default)',
|
||||
value: 'canRead'
|
||||
},
|
||||
{
|
||||
label: 'Write',
|
||||
value: 'canWrite'
|
||||
},
|
||||
{
|
||||
label: 'Update',
|
||||
value: 'canUpdate'
|
||||
},
|
||||
{
|
||||
label: 'Delete',
|
||||
value: 'canDelete'
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
label="Permissions"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<>
|
||||
{options.map(({ label, value: optionValue }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
id={value[optionValue]}
|
||||
key={optionValue}
|
||||
className="data-[state=checked]:bg-primary"
|
||||
isChecked={value[optionValue]}
|
||||
isDisabled={optionValue === 'read'}
|
||||
onCheckedChange={(state) => {
|
||||
onChange({
|
||||
...value,
|
||||
[optionValue]: state
|
||||
});
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Checkbox>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
type="submit"
|
||||
isDisabled={isSubmitting}
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button variant="plain" colorSchema="secondary">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.removeProjectLevelPermissions.isOpen}
|
||||
deleteKey="remove"
|
||||
title="Do you want to remove this permission from the service account?"
|
||||
onChange={(isOpen) => handlePopUpToggle('removeProjectLevelPermissions', isOpen)}
|
||||
onDeleteApproved={onRemoveProjectLevelPermissions}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { SAProjectLevelPermissionsTable } from './SAProjectLevelPermissionsTable';
|
||||
@@ -0,0 +1,88 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { faCheck } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import * as yup from 'yup';
|
||||
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Input} from '@app/components/v2';
|
||||
import {
|
||||
useGetServiceAccountById,
|
||||
useRenameServiceAccount
|
||||
} from '@app/hooks/api';
|
||||
|
||||
const formSchema = yup.object({
|
||||
name: yup.string().required().label('Service Account Name')
|
||||
});
|
||||
|
||||
type FormData = yup.InferType<typeof formSchema>;
|
||||
|
||||
type Props = {
|
||||
serviceAccountId: string;
|
||||
}
|
||||
|
||||
export const ServiceAccountNameChangeSection = ({
|
||||
serviceAccountId
|
||||
}: Props) => {
|
||||
const { data: serviceAccount, isLoading: isServiceAccountLoading } = useGetServiceAccountById(serviceAccountId);
|
||||
|
||||
const renameServiceAccount = useRenameServiceAccount();
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
reset,
|
||||
formState: { isDirty, isSubmitting }
|
||||
} = useForm<FormData>({ resolver: yupResolver(formSchema) });
|
||||
|
||||
useEffect(() => {
|
||||
reset({ name: serviceAccount?.name });
|
||||
}, [serviceAccount?.name]);
|
||||
|
||||
const onFormSubmit = async ({ name }: FormData) => {
|
||||
try {
|
||||
await renameServiceAccount.mutateAsync({
|
||||
serviceAccountId,
|
||||
name
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onFormSubmit)}
|
||||
className="rounded-md bg-white/5 p-6"
|
||||
>
|
||||
<p className="mb-4 text-xl font-semibold">Service Account Name</p>
|
||||
<div className="mb-2 w-full max-w-lg">
|
||||
{!isServiceAccountLoading && (
|
||||
<Controller
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input placeholder="Type your service account name..." {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="name"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
isLoading={isSubmitting}
|
||||
color="mineshaft"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isDisabled={!isDirty || isSubmitting}
|
||||
leftIcon={<FontAwesomeIcon icon={faCheck} />}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { ServiceAccountNameChangeSection } from './ServiceAccountNameChangeSection';
|
||||
@@ -0,0 +1,2 @@
|
||||
export { SAProjectLevelPermissionsTable } from './SAProjectLevelPermissionsTable';
|
||||
export { ServiceAccountNameChangeSection } from './ServiceAccountNameChangeSection';
|
||||
@@ -0,0 +1 @@
|
||||
export { CreateServiceAccountPage } from './CreateServiceAccountPage';
|
||||
@@ -20,10 +20,15 @@ import {
|
||||
useGetUserWsKey,
|
||||
useRenameOrg,
|
||||
useUpdateOrgUserRole,
|
||||
useUploadWsKey
|
||||
useUploadWsKey,
|
||||
} from '@app/hooks/api';
|
||||
|
||||
import { OrgIncidentContactsTable, OrgMembersTable, OrgNameChangeSection } from './components';
|
||||
import {
|
||||
OrgIncidentContactsTable,
|
||||
OrgMembersTable,
|
||||
OrgNameChangeSection,
|
||||
OrgServiceAccountsTable
|
||||
} from './components';
|
||||
|
||||
export const OrgSettingsPage = () => {
|
||||
const host = window.location.origin;
|
||||
@@ -36,12 +41,11 @@ export const OrgSettingsPage = () => {
|
||||
const { createNotification } = useNotificationContext();
|
||||
|
||||
const orgId = currentOrg?._id || '';
|
||||
|
||||
const { data: orgUsers, isLoading: isOrgUserLoading } = useGetOrgUsers(orgId);
|
||||
const { data: workspaceMemberships, isLoading: IsWsMembershipLoading } =
|
||||
useGetUserWorkspaceMemberships(orgId);
|
||||
const { data: workspaceMemberships, isLoading: IsWsMembershipLoading } = useGetUserWorkspaceMemberships(orgId);
|
||||
const { data: wsKey } = useGetUserWsKey(currentWorkspace?._id || '');
|
||||
const { data: incidentContact, isLoading: IsIncidentContactLoading } =
|
||||
useGetOrgIncidentContact(orgId);
|
||||
const { data: incidentContact, isLoading: IsIncidentContactLoading } = useGetOrgIncidentContact(orgId);
|
||||
|
||||
const renameOrg = useRenameOrg();
|
||||
const removeUserOrgMembership = useDeleteOrgMembership();
|
||||
@@ -222,17 +226,15 @@ export const OrgSettingsPage = () => {
|
||||
return (
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<NavHeader pageName={t('settings-org:title')} />
|
||||
<div className="my-8 ml-6 flex max-w-5xl flex-row items-center justify-between text-xl">
|
||||
<div className="flex flex-col items-start justify-start text-3xl">
|
||||
<p className="mr-4 font-semibold text-gray-200">{t('settings-org:title')}</p>
|
||||
<p className="mr-4 text-base font-normal text-gray-400">
|
||||
{t('settings-org:description')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="my-8 max-w-5xl ml-8">
|
||||
<p className="text-3xl font-semibold text-gray-200">{t('settings-org:title')}</p>
|
||||
<p className="text-base font-normal text-gray-400">
|
||||
{t('settings-org:description')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="max-w-8xl ml-6 mr-6 flex flex-col text-mineshaft-50">
|
||||
<OrgNameChangeSection orgName={currentOrg?.name} onOrgNameChange={onRenameOrg} />
|
||||
<div className="mb-6 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pt-6 pb-6">
|
||||
<div className="w-full rounded-md bg-white/5 p-6 mb-6">
|
||||
<p className="mr-4 mb-4 text-xl font-semibold text-white">
|
||||
{t('section-members:org-members')}
|
||||
</p>
|
||||
@@ -249,6 +251,12 @@ export const OrgSettingsPage = () => {
|
||||
onGrantAccess={onGrantUserAccess}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-6 mt-2 w-full rounded-md bg-white/5 p-6">
|
||||
<p className="mr-4 mb-4 text-xl font-semibold text-white">
|
||||
Service Accounts
|
||||
</p>
|
||||
<OrgServiceAccountsTable />
|
||||
</div>
|
||||
<div className="mb-6 mt-2 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pt-6 pb-6">
|
||||
<div className="flex w-full max-w-5xl flex-row items-center justify-between">
|
||||
<div className="flex w-full max-w-3xl flex-col justify-between">
|
||||
|
||||
@@ -26,7 +26,8 @@ import {
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr} from '@app/components/v2';
|
||||
Tr
|
||||
} from '@app/components/v2';
|
||||
import { usePopUp } from '@app/hooks';
|
||||
import { IncidentContact } from '@app/hooks/api/types';
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ export const OrgMembersTable = ({
|
||||
() => members.find(({ user }) => userId === user?._id)?.role === 'owner',
|
||||
[userId, members]
|
||||
);
|
||||
|
||||
|
||||
const filterdUser = useMemo(
|
||||
() =>
|
||||
members.filter(
|
||||
@@ -117,20 +117,18 @@ export const OrgMembersTable = ({
|
||||
placeholder="Search members..."
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
if (isMoreUserNotAllowed) {
|
||||
handlePopUpOpen('upgradePlan');
|
||||
} else {
|
||||
handlePopUpOpen('addMember');
|
||||
}
|
||||
}}
|
||||
>
|
||||
Add Member
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
if (isMoreUserNotAllowed) {
|
||||
handlePopUpOpen('upgradePlan');
|
||||
} else {
|
||||
handlePopUpOpen('addMember');
|
||||
}
|
||||
}}
|
||||
>
|
||||
Add Member
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<TableContainer>
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
import { useEffect, useMemo,useState } from 'react';
|
||||
import { Controller,useForm } from 'react-hook-form';
|
||||
import { useRouter } from 'next/router';
|
||||
import {
|
||||
faCheck,
|
||||
faCopy,
|
||||
faMagnifyingGlass,
|
||||
faPencil,
|
||||
faPlus,
|
||||
faServer,
|
||||
faTrash} from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import * as yup from 'yup';
|
||||
|
||||
import { generateKeyPair } from '@app/components/utilities/cryptography/crypto';
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
EmptyState,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from '@app/components/v2';
|
||||
import { useOrganization, useWorkspace } from '@app/context';
|
||||
import { usePopUp, useToggle } from '@app/hooks';
|
||||
import {
|
||||
useCreateServiceAccount,
|
||||
useDeleteServiceAccount,
|
||||
useGetServiceAccounts} from '@app/hooks/api';
|
||||
|
||||
const serviceAccountExpiration = [
|
||||
{ label: '1 Day', value: 86400 },
|
||||
{ label: '7 Days', value: 604800 },
|
||||
{ label: '1 Month', value: 2592000 },
|
||||
{ label: '6 months', value: 15552000 },
|
||||
{ label: '12 months', value: 31104000 },
|
||||
{ label: 'Never', value: -1 }
|
||||
];
|
||||
|
||||
const addServiceAccountFormSchema = yup.object({
|
||||
name: yup.string().required().label('Name').trim(),
|
||||
expiresIn: yup.string().required().label('Service Account Expiration')
|
||||
});
|
||||
|
||||
type TAddServiceAccountForm = yup.InferType<typeof addServiceAccountFormSchema>;
|
||||
|
||||
export const OrgServiceAccountsTable = () => {
|
||||
const router = useRouter();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
console.log('currentWorkspace: ', currentWorkspace);
|
||||
|
||||
const orgId = currentOrg?._id || '';
|
||||
const [step, setStep] = useState(0);
|
||||
const [isAccessKeyCopied, setIsAccessKeyCopied] = useToggle(false);
|
||||
const [isPrivateKeyCopied, setIsPrivateKeyCopied] = useToggle(false);
|
||||
const [accessKey, setAccessKey] = useState('');
|
||||
const [privateKey, setPrivateKey] = useState('');
|
||||
const [searchServiceAccountFilter, setSearchServiceAccountFilter] = useState('');
|
||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||
'addServiceAccount',
|
||||
'removeServiceAccount',
|
||||
] as const);
|
||||
|
||||
const { data: serviceAccounts = [], isLoading: isServiceAccountsLoading } = useGetServiceAccounts(orgId);
|
||||
|
||||
const createServiceAccount = useCreateServiceAccount();
|
||||
const removeServiceAccount = useDeleteServiceAccount();
|
||||
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout;
|
||||
if (isAccessKeyCopied) {
|
||||
timer = setTimeout(() => setIsAccessKeyCopied.off(), 2000);
|
||||
}
|
||||
|
||||
if (isPrivateKeyCopied) {
|
||||
timer = setTimeout(() => setIsPrivateKeyCopied.off(), 2000);
|
||||
}
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [isAccessKeyCopied, isPrivateKeyCopied]);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<TAddServiceAccountForm>({ resolver: yupResolver(addServiceAccountFormSchema) });
|
||||
|
||||
const onAddServiceAccount = async ({ name, expiresIn }: TAddServiceAccountForm) => {
|
||||
if (!currentOrg?._id) return;
|
||||
|
||||
const keyPair = generateKeyPair();
|
||||
setPrivateKey(keyPair.privateKey);
|
||||
|
||||
const serviceAccountDetails = await createServiceAccount.mutateAsync({
|
||||
name,
|
||||
organizationId: currentOrg?._id,
|
||||
publicKey: keyPair.publicKey,
|
||||
expiresIn
|
||||
});
|
||||
|
||||
console.log('serviceAccountDetails: ', serviceAccountDetails);
|
||||
|
||||
setAccessKey(serviceAccountDetails.serviceAccountAccessKey);
|
||||
|
||||
setStep(1);
|
||||
reset();
|
||||
}
|
||||
|
||||
const onRemoveServiceAccount = async () => {
|
||||
console.log('onRemoveServiceAccount');
|
||||
|
||||
const serviceAccountId = (popUp?.removeServiceAccount?.data as { _id: string })?._id;
|
||||
console.log('serviceAccountId: ', serviceAccountId);
|
||||
|
||||
await removeServiceAccount.mutateAsync(serviceAccountId);
|
||||
handlePopUpClose('removeServiceAccount');
|
||||
}
|
||||
|
||||
const filteredServiceAccounts = useMemo(
|
||||
() =>
|
||||
serviceAccounts.filter(
|
||||
({ name }) =>
|
||||
name.toLowerCase().includes(searchServiceAccountFilter)
|
||||
),
|
||||
[serviceAccounts, searchServiceAccountFilter]
|
||||
);
|
||||
|
||||
const renderStep = (stepToRender: number) => {
|
||||
switch (stepToRender) {
|
||||
case 0:
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onAddServiceAccount)}>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="name"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Name" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="expiresIn"
|
||||
defaultValue={String(serviceAccountExpiration?.[0]?.value)}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Expiration"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{serviceAccountExpiration.map(({ label, value }) => (
|
||||
<SelectItem value={String(value)} key={label}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Create Service Account
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpClose('addServiceAccount')}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
case 1:
|
||||
return (
|
||||
<>
|
||||
<p>Access Key</p>
|
||||
<div className="flex items-center justify-end rounded-md p-2 text-base text-gray-400 bg-white/[0.07]">
|
||||
<p className="mr-4 break-all">{accessKey}</p>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(accessKey);
|
||||
setIsAccessKeyCopied.on();
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isAccessKeyCopied ? faCheck : faCopy} />
|
||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
Copy
|
||||
</span>
|
||||
</IconButton>
|
||||
</div>
|
||||
<p className="mt-4">Private Key</p>
|
||||
<div className="flex items-center justify-end rounded-md p-2 text-base text-gray-400 bg-white/[0.07]">
|
||||
<p className="mr-4 break-all">{privateKey}</p>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(privateKey);
|
||||
setIsPrivateKeyCopied.on();
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isPrivateKeyCopied ? faCheck : faCopy} />
|
||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
Copy
|
||||
</span>
|
||||
</IconButton>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return <div />
|
||||
}
|
||||
}
|
||||
|
||||
console.log('serviceAccounts: ', serviceAccounts);
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="mb-4 flex">
|
||||
<div className="mr-4 flex-1">
|
||||
<Input
|
||||
value={searchServiceAccountFilter}
|
||||
onChange={(e) => setSearchServiceAccountFilter(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search service accounts..."
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
setStep(0);
|
||||
reset();
|
||||
handlePopUpOpen('addServiceAccount');
|
||||
}}
|
||||
>
|
||||
Add Service Account
|
||||
</Button>
|
||||
</div>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Th>Name</Th>
|
||||
<Th className="w-full">Valid Until</Th>
|
||||
<Th aria-label="actions" />
|
||||
</THead>
|
||||
<TBody>
|
||||
{isServiceAccountsLoading && <TableSkeleton columns={5} key="org-service-accounts" />}
|
||||
{!isServiceAccountsLoading && (
|
||||
filteredServiceAccounts.map(({
|
||||
name,
|
||||
expiresAt,
|
||||
_id: serviceAccountId
|
||||
}) => {
|
||||
return (
|
||||
<Tr key={`org-service-account-${serviceAccountId}`}>
|
||||
<Td>{name}</Td>
|
||||
<Td>{new Date(expiresAt).toUTCString()}</Td>
|
||||
<Td>
|
||||
<div className="flex">
|
||||
<IconButton
|
||||
ariaLabel="edit"
|
||||
colorSchema="secondary"
|
||||
onClick={() => {
|
||||
if (currentWorkspace?._id) {
|
||||
router.push(`/settings/org/${currentWorkspace._id}/service-accounts/${serviceAccountId}`);
|
||||
}
|
||||
}}
|
||||
className="mr-2"
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencil} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
ariaLabel="delete"
|
||||
colorSchema="danger"
|
||||
onClick={() => handlePopUpOpen('removeServiceAccount', { _id: serviceAccountId })}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
{!isServiceAccountsLoading && filteredServiceAccounts?.length === 0 && (
|
||||
<Tr>
|
||||
<Td colSpan={4} className="text-center">
|
||||
<EmptyState title="No service accounts found" icon={faServer} />
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Modal
|
||||
isOpen={popUp?.addServiceAccount?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle('addServiceAccount', isOpen);
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
<ModalContent
|
||||
title="Add Service Account"
|
||||
subTitle="A service account represents a machine identity such as a VM or application client."
|
||||
>
|
||||
{renderStep(step)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.removeServiceAccount.isOpen}
|
||||
deleteKey="remove"
|
||||
title="Do you want to remove this service account from the org?"
|
||||
onChange={(isOpen) => handlePopUpToggle('removeServiceAccount', isOpen)}
|
||||
onDeleteApproved={onRemoveServiceAccount}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { OrgServiceAccountsTable } from './OrgServiceAccountsTable';
|
||||
@@ -1,3 +1,5 @@
|
||||
export { OrgIncidentContactsTable } from './OrgIncidentContactsTable';
|
||||
export { OrgMembersTable } from './OrgMembersTable';
|
||||
export { OrgNameChangeSection } from './OrgNameChangeSection';
|
||||
export { OrgServiceAccountsTable } from './OrgServiceAccountsTable';
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ const apiTokenExpiry = [
|
||||
const createServiceTokenSchema = yup.object({
|
||||
name: yup.string().required().label('Service Token Name'),
|
||||
environment: yup.string().required().label('Environment'),
|
||||
expiresIn: yup.string().required().label('Service Token Name'),
|
||||
expiresIn: yup.string().required().label('Service Token Expiration'),
|
||||
permissions: yup.object().shape({
|
||||
read: yup.boolean().required(),
|
||||
write: yup.boolean().required()
|
||||
@@ -202,7 +202,7 @@ export const ServiceTokenSection = ({
|
||||
defaultValue={String(apiTokenExpiry?.[0]?.value)}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Token Expiry"
|
||||
label="Expiration"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
@@ -269,42 +269,6 @@ export const ServiceTokenSection = ({
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{/* <Controller
|
||||
name="isReadEnabled"
|
||||
defaultValue={true}
|
||||
control={control}
|
||||
render={({ field: { onChange, ... field }, fieldState }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
className="data-[state=checked]:bg-primary"
|
||||
isChecked={field.value}
|
||||
onCheckedChange={(state) => {
|
||||
onChange(state);
|
||||
}}
|
||||
>
|
||||
Read (default)
|
||||
</Checkbox>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
name="isWriteEnabled"
|
||||
defaultValue={false}
|
||||
control={control}
|
||||
render={({ field: { onChange, ... field }, fieldState }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
className="data-[state=checked]:bg-primary"
|
||||
isChecked={field.value}
|
||||
onCheckedChange={(state) => {
|
||||
onChange(state);
|
||||
}}
|
||||
>
|
||||
Write (optional)
|
||||
</Checkbox>
|
||||
);
|
||||
}}
|
||||
/> */}
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
|
||||
Reference in New Issue
Block a user