mirror of
https://github.com/Unleash/unleash.git
synced 2025-03-23 00:16:25 +01:00
chore: delete project api tokens when last mapped project is removed (#7503)
Deletes API tokens bound to specific projects when the last project they're mapped to is deleted. --------- Co-authored-by: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Co-authored-by: Thomas Heartman <thomas@getunleash.io>
This commit is contained in:
parent
f6c05eb877
commit
2e5d81cb89
src/lib
__snapshots__
features/project
types
@ -82,6 +82,7 @@ exports[`should create default config 1`] = `
|
||||
"automatedActions": false,
|
||||
"caseInsensitiveInOperators": false,
|
||||
"celebrateUnleash": false,
|
||||
"cleanApiTokenWhenOrphaned": false,
|
||||
"collectTrafficDataUsage": false,
|
||||
"commandBarUI": false,
|
||||
"demo": false,
|
||||
|
@ -45,6 +45,8 @@ import { ProjectOwnersReadModel } from './project-owners-read-model';
|
||||
import { FakeProjectOwnersReadModel } from './fake-project-owners-read-model';
|
||||
import { FakeProjectFlagCreatorsReadModel } from './fake-project-flag-creators-read-model';
|
||||
import { ProjectFlagCreatorsReadModel } from './project-flag-creators-read-model';
|
||||
import FakeApiTokenStore from '../../../test/fixtures/fake-api-token-store';
|
||||
import { ApiTokenStore } from '../../db/api-token-store';
|
||||
|
||||
export const createProjectService = (
|
||||
db: Db,
|
||||
@ -109,6 +111,13 @@ export const createProjectService = (
|
||||
eventService,
|
||||
);
|
||||
|
||||
const apiTokenStore = new ApiTokenStore(
|
||||
db,
|
||||
eventBus,
|
||||
getLogger,
|
||||
flagResolver,
|
||||
);
|
||||
|
||||
const privateProjectChecker = createPrivateProjectChecker(db, config);
|
||||
|
||||
return new ProjectService(
|
||||
@ -123,6 +132,7 @@ export const createProjectService = (
|
||||
projectStatsStore,
|
||||
projectOwnersReadModel,
|
||||
projectFlagCreatorsReadModel,
|
||||
apiTokenStore,
|
||||
},
|
||||
config,
|
||||
accessService,
|
||||
@ -153,6 +163,7 @@ export const createFakeProjectService = (
|
||||
const { featureToggleService } = createFakeFeatureToggleService(config);
|
||||
const favoriteFeaturesStore = new FakeFavoriteFeaturesStore();
|
||||
const favoriteProjectsStore = new FakeFavoriteProjectsStore();
|
||||
const apiTokenStore = new FakeApiTokenStore();
|
||||
const eventService = new EventService(
|
||||
{
|
||||
eventStore,
|
||||
@ -188,6 +199,7 @@ export const createFakeProjectService = (
|
||||
featureTypeStore,
|
||||
accountStore,
|
||||
projectStatsStore,
|
||||
apiTokenStore,
|
||||
},
|
||||
config,
|
||||
accessService,
|
||||
|
@ -9,7 +9,7 @@ import { RoleName } from '../../types/model';
|
||||
import { randomId } from '../../util/random-id';
|
||||
import EnvironmentService from '../project-environments/environment-service';
|
||||
import IncompatibleProjectError from '../../error/incompatible-project-error';
|
||||
import { EventService } from '../../services';
|
||||
import { ApiTokenService, EventService } from '../../services';
|
||||
import { FeatureEnvironmentEvent } from '../../types/events';
|
||||
import { addDays, subDays } from 'date-fns';
|
||||
import {
|
||||
@ -28,7 +28,8 @@ import {
|
||||
} from '../../types';
|
||||
import type { User } from '../../server-impl';
|
||||
import { BadDataError, InvalidOperationError } from '../../error';
|
||||
import { extractAuditInfoFromUser } from '../../util';
|
||||
import { DEFAULT_ENV, extractAuditInfoFromUser } from '../../util';
|
||||
import { ApiTokenType } from '../../types/models/api-token';
|
||||
|
||||
let stores: IUnleashStores;
|
||||
let db: ITestDb;
|
||||
@ -40,6 +41,7 @@ let environmentService: EnvironmentService;
|
||||
let featureToggleService: FeatureToggleService;
|
||||
let user: User; // many methods in this test use User instead of IUser
|
||||
let auditUser: IAuditUser;
|
||||
let apiTokenService: ApiTokenService;
|
||||
|
||||
let opsUser: IUser;
|
||||
let group: IGroup;
|
||||
@ -89,6 +91,7 @@ beforeAll(async () => {
|
||||
|
||||
environmentService = new EnvironmentService(stores, config, eventService);
|
||||
projectService = createProjectService(db.rawDatabase, config);
|
||||
apiTokenService = new ApiTokenService(stores, config, eventService);
|
||||
});
|
||||
beforeEach(async () => {
|
||||
await stores.accessStore.addUserToRole(opsUser.id, 1, '');
|
||||
@ -2488,6 +2491,103 @@ test('deleting a project with archived flags should result in any remaining arch
|
||||
expect(flags.find((t) => t.name === flagName)).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should also delete api tokens that were only bound to deleted project', async () => {
|
||||
const project = 'some';
|
||||
const tokenName = 'test';
|
||||
|
||||
await projectService.createProject(
|
||||
{
|
||||
id: project,
|
||||
name: 'Test Project 1',
|
||||
},
|
||||
user,
|
||||
auditUser,
|
||||
);
|
||||
|
||||
const token = await apiTokenService.createApiToken({
|
||||
type: ApiTokenType.CLIENT,
|
||||
tokenName,
|
||||
environment: DEFAULT_ENV,
|
||||
project: project,
|
||||
});
|
||||
|
||||
await projectService.deleteProject(project, user, auditUser);
|
||||
const deletedToken = await apiTokenService.getToken(token.secret);
|
||||
expect(deletedToken).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should not delete project-bound api tokens still bound to project', async () => {
|
||||
const project1 = 'token-deleted-project';
|
||||
const project2 = 'token-not-deleted-project';
|
||||
const tokenName = 'test';
|
||||
|
||||
await projectService.createProject(
|
||||
{
|
||||
id: project1,
|
||||
name: 'Test Project 1',
|
||||
},
|
||||
user,
|
||||
auditUser,
|
||||
);
|
||||
|
||||
await projectService.createProject(
|
||||
{
|
||||
id: project2,
|
||||
name: 'Test Project 2',
|
||||
},
|
||||
user,
|
||||
auditUser,
|
||||
);
|
||||
|
||||
const token = await apiTokenService.createApiToken({
|
||||
type: ApiTokenType.CLIENT,
|
||||
tokenName,
|
||||
environment: DEFAULT_ENV,
|
||||
projects: [project1, project2],
|
||||
});
|
||||
|
||||
await projectService.deleteProject(project1, user, auditUser);
|
||||
const fetchedToken = await apiTokenService.getToken(token.secret);
|
||||
expect(fetchedToken).not.toBeUndefined();
|
||||
expect(fetchedToken.project).toBe(project2);
|
||||
});
|
||||
|
||||
test('should delete project-bound api tokens when all projects they belong to are deleted', async () => {
|
||||
const project1 = 'token-deleted-project-1';
|
||||
const project2 = 'token-deleted-project-2';
|
||||
const tokenName = 'test';
|
||||
|
||||
await projectService.createProject(
|
||||
{
|
||||
id: project1,
|
||||
name: 'Test Project 1',
|
||||
},
|
||||
user,
|
||||
auditUser,
|
||||
);
|
||||
|
||||
await projectService.createProject(
|
||||
{
|
||||
id: project2,
|
||||
name: 'Test Project 2',
|
||||
},
|
||||
user,
|
||||
auditUser,
|
||||
);
|
||||
|
||||
const token = await apiTokenService.createApiToken({
|
||||
type: ApiTokenType.CLIENT,
|
||||
tokenName,
|
||||
environment: DEFAULT_ENV,
|
||||
projects: [project1, project2],
|
||||
});
|
||||
|
||||
await projectService.deleteProject(project1, user, auditUser);
|
||||
await projectService.deleteProject(project2, user, auditUser);
|
||||
const fetchedToken = await apiTokenService.getToken(token.secret);
|
||||
expect(fetchedToken).toBeUndefined();
|
||||
});
|
||||
|
||||
test('deleting a project with no archived flags should not result in an error', async () => {
|
||||
const project = {
|
||||
id: 'project-with-nothing',
|
||||
|
@ -53,6 +53,7 @@ import {
|
||||
type ProjectCreated,
|
||||
type IProjectOwnersReadModel,
|
||||
ADMIN,
|
||||
type IApiTokenStore,
|
||||
} from '../../types';
|
||||
import type {
|
||||
IProjectAccessModel,
|
||||
@ -144,6 +145,8 @@ export default class ProjectService {
|
||||
|
||||
private accountStore: IAccountStore;
|
||||
|
||||
private apiTokenStore: IApiTokenStore;
|
||||
|
||||
private favoritesService: FavoritesService;
|
||||
|
||||
private eventService: EventService;
|
||||
@ -168,6 +171,7 @@ export default class ProjectService {
|
||||
featureTypeStore,
|
||||
accountStore,
|
||||
projectStatsStore,
|
||||
apiTokenStore,
|
||||
}: Pick<
|
||||
IUnleashStores,
|
||||
| 'projectStore'
|
||||
@ -180,6 +184,7 @@ export default class ProjectService {
|
||||
| 'accountStore'
|
||||
| 'projectStatsStore'
|
||||
| 'featureTypeStore'
|
||||
| 'apiTokenStore'
|
||||
>,
|
||||
config: IUnleashConfig,
|
||||
accessService: AccessService,
|
||||
@ -198,6 +203,7 @@ export default class ProjectService {
|
||||
this.eventStore = eventStore;
|
||||
this.featureToggleStore = featureToggleStore;
|
||||
this.featureTypeStore = featureTypeStore;
|
||||
this.apiTokenStore = apiTokenStore;
|
||||
this.featureToggleService = featureToggleService;
|
||||
this.favoritesService = favoriteService;
|
||||
this.privateProjectChecker = privateProjectChecker;
|
||||
@ -558,7 +564,26 @@ export default class ProjectService {
|
||||
auditUser,
|
||||
);
|
||||
|
||||
await this.projectStore.delete(id);
|
||||
if (this.flagResolver.isEnabled('cleanApiTokenWhenOrphaned')) {
|
||||
const allTokens = await this.apiTokenStore.getAll();
|
||||
const projectTokens = allTokens.filter(
|
||||
(token) =>
|
||||
(token.projects &&
|
||||
token.projects.length === 1 &&
|
||||
token.projects[0] === id) ||
|
||||
token.project === id,
|
||||
);
|
||||
|
||||
await this.projectStore.delete(id);
|
||||
|
||||
await Promise.all(
|
||||
projectTokens.map((token) =>
|
||||
this.apiTokenStore.delete(token.secret),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
await this.projectStore.delete(id);
|
||||
}
|
||||
|
||||
await this.eventService.storeEvent(
|
||||
new ProjectDeletedEvent({
|
||||
|
@ -63,8 +63,9 @@ export type IFlagKey =
|
||||
| 'flagCreator'
|
||||
| 'anonymizeProjectOwners'
|
||||
| 'resourceLimits'
|
||||
| 'allowOrphanedWildcardTokens'
|
||||
| 'extendedMetrics';
|
||||
| 'extendedMetrics'
|
||||
| 'cleanApiTokenWhenOrphaned'
|
||||
| 'allowOrphanedWildcardTokens';
|
||||
|
||||
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
||||
|
||||
@ -309,6 +310,10 @@ const flags: IFlags = {
|
||||
process.env.UNLEASH_EXPERIMENTAL_EXTENDED_METRICS,
|
||||
false,
|
||||
),
|
||||
cleanApiTokenWhenOrphaned: parseEnvVarBoolean(
|
||||
process.env.UNLEASH_EXPERIMENTAL_CLEAN_API_TOKEN_WHEN_ORPHANED,
|
||||
false,
|
||||
),
|
||||
};
|
||||
|
||||
export const defaultExperimentalOptions: IExperimentalOptions = {
|
||||
|
Loading…
Reference in New Issue
Block a user