@flowcore/cli-plugin-iam 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,158 @@
1
+ import { baseResourceDto } from "@flowcore/cli-plugin-core";
2
+ import { diff } from "@opentf/obj-diff";
3
+ import enquirer from "enquirer";
4
+ import { diffString } from "json-diff";
5
+ import { omit } from "radash";
6
+ import { z } from "zod";
7
+ export var PolicyDocumentAction;
8
+ (function (PolicyDocumentAction) {
9
+ PolicyDocumentAction["ALL"] = "*";
10
+ PolicyDocumentAction["FETCH"] = "FETCH";
11
+ PolicyDocumentAction["INGEST"] = "INGEST";
12
+ PolicyDocumentAction["READ"] = "READ";
13
+ PolicyDocumentAction["WRITE"] = "WRITE";
14
+ })(PolicyDocumentAction || (PolicyDocumentAction = {}));
15
+ export const policyDto = baseResourceDto.extend({
16
+ spec: z.object({
17
+ description: z.string().optional(),
18
+ flowcoreManaged: z.boolean().optional(),
19
+ policyDocuments: z.array(z.object({
20
+ action: z
21
+ .array(z.nativeEnum(PolicyDocumentAction))
22
+ .or(z.nativeEnum(PolicyDocumentAction)),
23
+ resource: z.string(),
24
+ statementId: z.string().optional(),
25
+ })),
26
+ principal: z.string().optional(),
27
+ version: z.string(),
28
+ }),
29
+ });
30
+ export class PolicyService {
31
+ iamClient;
32
+ logger;
33
+ getToken;
34
+ constructor(iamClient, logger, getToken) {
35
+ this.iamClient = iamClient;
36
+ this.logger = logger;
37
+ this.getToken = getToken;
38
+ }
39
+ async createNewPolicy(organizationId, policy, skipConfirmation) {
40
+ const parsedPolicy = policyDto.parse(policy);
41
+ try {
42
+ const policies = await this.iamClient.getApiV1PolicyAssociationsOrganizationByOrganizationId(organizationId, {
43
+ headers: {
44
+ Authorization: `Bearer ${await this.getToken()}`,
45
+ },
46
+ });
47
+ const existingPolicy = policies.data.find((p) => p.name === parsedPolicy.metadata.name);
48
+ if (!existingPolicy) {
49
+ const result = await this.iamClient.postApiV1Policies({
50
+ description: parsedPolicy.spec.description ?? undefined,
51
+ name: parsedPolicy.metadata.name,
52
+ organizationId,
53
+ policyDocuments: parsedPolicy.spec.policyDocuments,
54
+ principal: parsedPolicy.spec.principal ?? undefined,
55
+ version: parsedPolicy.spec.version,
56
+ }, {
57
+ headers: {
58
+ Authorization: `Bearer ${await this.getToken()}`,
59
+ },
60
+ });
61
+ if (result.status !== 200) {
62
+ this.logger.fatal(`Failed to create policy: ${result.statusText}`);
63
+ }
64
+ return true;
65
+ }
66
+ let useStatementIds = false;
67
+ const newPolicy = {
68
+ description: parsedPolicy.spec.description ?? "",
69
+ name: parsedPolicy.metadata.name,
70
+ organizationId,
71
+ policyDocuments: parsedPolicy.spec.policyDocuments.map((doc) => {
72
+ if (doc.statementId) {
73
+ useStatementIds = true;
74
+ }
75
+ return {
76
+ ...doc,
77
+ };
78
+ }),
79
+ version: parsedPolicy.spec.version,
80
+ ...(parsedPolicy.spec.principal && {
81
+ principal: parsedPolicy.spec.principal,
82
+ }),
83
+ flowcoreManaged: parsedPolicy.spec.flowcoreManaged ?? false,
84
+ };
85
+ if (diff({
86
+ ...omit(existingPolicy, ["id"]),
87
+ policyDocuments: existingPolicy.policyDocuments.map((doc) => useStatementIds ? doc : omit(doc, ["statementId"])),
88
+ }, newPolicy).length === 0) {
89
+ return false;
90
+ }
91
+ if (!skipConfirmation) {
92
+ this.logger.info("Policy has changed, do you want to apply these changes?");
93
+ this.logger.info(diffString({
94
+ ...omit(existingPolicy, ["id"]),
95
+ policyDocuments: existingPolicy.policyDocuments.map((doc) => useStatementIds ? doc : omit(doc, ["statementId"])),
96
+ }, newPolicy, { color: true, full: true }));
97
+ const { confirm } = await enquirer.prompt({
98
+ message: "Are you sure you want to update the policy?",
99
+ name: "confirm",
100
+ type: "confirm",
101
+ });
102
+ if (!confirm) {
103
+ return false;
104
+ }
105
+ }
106
+ const result = await this.iamClient.patchApiV1PoliciesById(existingPolicy.id, newPolicy, {
107
+ headers: {
108
+ Authorization: `Bearer ${await this.getToken()}`,
109
+ },
110
+ });
111
+ if (result.status !== 200) {
112
+ this.logger.fatal(`Failed to update policy: ${result.statusText}`);
113
+ }
114
+ return true;
115
+ }
116
+ catch (error) {
117
+ if (typeof error === "object" && error !== null && "error" in error) {
118
+ const err = error;
119
+ this.logger.fatal(`Failed to create policy with error(${err.error.status} - ${err.error.code}): ${err.error.message}`);
120
+ }
121
+ else {
122
+ this.logger.fatal(`Failed to create policy with unknown error: ${error}`);
123
+ }
124
+ }
125
+ }
126
+ async deletePolicy(organizationId, policy) {
127
+ const parsedPolicy = policyDto.parse(policy);
128
+ try {
129
+ const policies = await this.iamClient.getApiV1PolicyAssociationsOrganizationByOrganizationId(organizationId, {
130
+ headers: {
131
+ Authorization: `Bearer ${await this.getToken()}`,
132
+ },
133
+ });
134
+ const existingPolicy = policies.data.find((p) => p.name === parsedPolicy.metadata.name);
135
+ if (!existingPolicy) {
136
+ return false;
137
+ }
138
+ const result = await this.iamClient.deleteApiV1PoliciesById(existingPolicy.id, {
139
+ headers: {
140
+ Authorization: `Bearer ${await this.getToken()}`,
141
+ },
142
+ });
143
+ if (result.status !== 200) {
144
+ this.logger.fatal(`Failed to delete policy: ${result.statusText}`);
145
+ }
146
+ return true;
147
+ }
148
+ catch (error) {
149
+ if (typeof error === "object" && error !== null && "error" in error) {
150
+ const err = error;
151
+ this.logger.fatal(`Failed to delete policy with error(${err.error.status} - ${err.error.code}): ${err.error.message}`);
152
+ }
153
+ else {
154
+ this.logger.fatal(`Failed to delete policy with unknown error: ${error}`);
155
+ }
156
+ }
157
+ }
158
+ }
@@ -0,0 +1,82 @@
1
+ import type { Logger } from "@flowcore/cli-plugin-config";
2
+ import { z } from "zod";
3
+ import type { Api as IamApi } from "../utils/clients/iam/Api.js";
4
+ export declare const roleBindingDto: z.ZodObject<z.objectUtil.extendShape<{
5
+ apiVersion: z.ZodString;
6
+ kind: z.ZodString;
7
+ metadata: z.ZodObject<{
8
+ name: z.ZodString;
9
+ tenant: z.ZodString;
10
+ }, "strip", z.ZodTypeAny, {
11
+ name: string;
12
+ tenant: string;
13
+ }, {
14
+ name: string;
15
+ tenant: string;
16
+ }>;
17
+ }, {
18
+ spec: z.ZodObject<{
19
+ role: z.ZodString;
20
+ subjects: z.ZodArray<z.ZodObject<{
21
+ id: z.ZodString;
22
+ type: z.ZodEnum<["user", "key"]>;
23
+ }, "strip", z.ZodTypeAny, {
24
+ id: string;
25
+ type: "key" | "user";
26
+ }, {
27
+ id: string;
28
+ type: "key" | "user";
29
+ }>, "many">;
30
+ }, "strip", z.ZodTypeAny, {
31
+ subjects: {
32
+ id: string;
33
+ type: "key" | "user";
34
+ }[];
35
+ role: string;
36
+ }, {
37
+ subjects: {
38
+ id: string;
39
+ type: "key" | "user";
40
+ }[];
41
+ role: string;
42
+ }>;
43
+ }>, "strip", z.ZodTypeAny, {
44
+ kind: string;
45
+ apiVersion: string;
46
+ metadata: {
47
+ name: string;
48
+ tenant: string;
49
+ };
50
+ spec: {
51
+ subjects: {
52
+ id: string;
53
+ type: "key" | "user";
54
+ }[];
55
+ role: string;
56
+ };
57
+ }, {
58
+ kind: string;
59
+ apiVersion: string;
60
+ metadata: {
61
+ name: string;
62
+ tenant: string;
63
+ };
64
+ spec: {
65
+ subjects: {
66
+ id: string;
67
+ type: "key" | "user";
68
+ }[];
69
+ role: string;
70
+ };
71
+ }>;
72
+ export type RoleBinding = z.infer<typeof roleBindingDto>;
73
+ export type ApiRoleResource = Awaited<ReturnType<IamApi["getApiV1RoleAssociationsOrganizationByOrganizationId"]>>["data"][0];
74
+ export type ApiRoleAssociationResource = Awaited<ReturnType<IamApi["getApiV1RoleAssociationsByRoleId"]>>["data"];
75
+ export declare class RoleBindingService {
76
+ private readonly iamClient;
77
+ private readonly logger;
78
+ private readonly getToken;
79
+ constructor(iamClient: IamApi, logger: Logger, getToken: () => Promise<string>);
80
+ createNewRoleBinding(organizationId: string, roleBinding: unknown, skipConfirmation: boolean): Promise<boolean>;
81
+ deleteRoleBinding(organizationId: string, roleBinding: unknown): Promise<boolean>;
82
+ }
@@ -0,0 +1,203 @@
1
+ import { baseResourceDto } from "@flowcore/cli-plugin-core";
2
+ import enquirer from "enquirer";
3
+ import { diffString } from "json-diff";
4
+ import { z } from "zod";
5
+ export const roleBindingDto = baseResourceDto.extend({
6
+ spec: z.object({
7
+ role: z.string(),
8
+ subjects: z.array(z.object({
9
+ id: z.string(),
10
+ type: z.enum(["user", "key"]),
11
+ })),
12
+ }),
13
+ });
14
+ export class RoleBindingService {
15
+ iamClient;
16
+ logger;
17
+ getToken;
18
+ constructor(iamClient, logger, getToken) {
19
+ this.iamClient = iamClient;
20
+ this.logger = logger;
21
+ this.getToken = getToken;
22
+ }
23
+ async createNewRoleBinding(organizationId, roleBinding, skipConfirmation) {
24
+ const parsedRoleBinding = roleBindingDto.parse(roleBinding);
25
+ try {
26
+ const roles = await this.iamClient.getApiV1RoleAssociationsOrganizationByOrganizationId(organizationId, {
27
+ headers: {
28
+ Authorization: `Bearer ${await this.getToken()}`,
29
+ },
30
+ });
31
+ const existingRole = roles.data.find((r) => r.name === parsedRoleBinding.spec.role);
32
+ if (!existingRole) {
33
+ this.logger.error(`Role ${parsedRoleBinding.spec.role} not found`);
34
+ return false;
35
+ }
36
+ const associations = await this.iamClient.getApiV1RoleAssociationsByRoleId(existingRole.id, {
37
+ headers: {
38
+ Authorization: `Bearer ${await this.getToken()}`,
39
+ },
40
+ });
41
+ const usersToDelete = [];
42
+ for (const userAssociation of associations.data.users.filter((u) => u.roleId !== existingRole.id)) {
43
+ if (!parsedRoleBinding.spec.subjects.find((s) => s.type === "user" && s.id === userAssociation.userId)) {
44
+ usersToDelete.push(userAssociation.userId);
45
+ }
46
+ }
47
+ const keysToDelete = [];
48
+ for (const keyAssociation of associations.data.keys.filter((k) => k.roleId !== existingRole.id)) {
49
+ if (!parsedRoleBinding.spec.subjects.find((s) => s.type === "key" && s.id === keyAssociation.keyId)) {
50
+ keysToDelete.push(keyAssociation.keyId);
51
+ }
52
+ }
53
+ if (!skipConfirmation) {
54
+ const diffObject = {
55
+ keys: associations.data.keys.filter((k) => !keysToDelete.includes(k.keyId)),
56
+ users: associations.data.users.filter((u) => !usersToDelete.includes(u.userId)),
57
+ };
58
+ this.logger.info("Modifying the following:");
59
+ this.logger.info(`${diffString(associations.data.users.map((u) => u.userId), diffObject.users, { color: true, full: true })}`);
60
+ this.logger.info(`${diffString(associations.data.keys.map((k) => k.keyId), diffObject.keys, { color: true, full: true })}`);
61
+ const { confirm } = await enquirer.prompt([
62
+ {
63
+ message: `Are you sure you want to remove ${usersToDelete.length} users and ${keysToDelete.length} keys from role ${existingRole.name}?`,
64
+ name: "confirm",
65
+ type: "confirm",
66
+ },
67
+ ]);
68
+ if (!confirm) {
69
+ return false;
70
+ }
71
+ }
72
+ let changed = keysToDelete.length > 0 || usersToDelete.length > 0;
73
+ for (const userId of usersToDelete) {
74
+ const result = await this.iamClient.deleteApiV1RoleAssociationsUserByUserId(userId, { roleId: existingRole.id }, {
75
+ headers: {
76
+ Authorization: `Bearer ${await this.getToken()}`,
77
+ },
78
+ });
79
+ if (result.status !== 200) {
80
+ this.logger.error(`Failed to delete user ${userId} from role ${existingRole.id}: ${result.statusText}`);
81
+ }
82
+ }
83
+ for (const keyId of keysToDelete) {
84
+ const result = await this.iamClient.deleteApiV1RoleAssociationsKeyByKeyId(keyId, { roleId: existingRole.id }, {
85
+ headers: {
86
+ Authorization: `Bearer ${await this.getToken()}`,
87
+ },
88
+ });
89
+ if (result.status !== 200) {
90
+ this.logger.error(`Failed to delete key ${keyId} from role ${existingRole.id}: ${result.statusText}`);
91
+ }
92
+ }
93
+ for (const subject of parsedRoleBinding.spec.subjects) {
94
+ switch (subject.type) {
95
+ case "user": {
96
+ const existingAssociation = associations.data.users.find((a) => a.userId === subject.id);
97
+ if (existingAssociation) {
98
+ this.logger.debug(`User ${subject.id} already bound to role ${existingRole.id}`);
99
+ continue;
100
+ }
101
+ const result = await this.iamClient.postApiV1RoleAssociationsUserByUserId(subject.id, { roleId: existingRole.id }, {
102
+ headers: {
103
+ Authorization: `Bearer ${await this.getToken()}`,
104
+ },
105
+ });
106
+ if (result.status !== 200) {
107
+ this.logger.error(`Failed to bind user ${subject.id} to role ${existingRole.id}: ${result.statusText}`);
108
+ }
109
+ changed = true;
110
+ break;
111
+ }
112
+ case "key": {
113
+ const existingAssociation = associations.data.keys.find((a) => a.keyId === subject.id);
114
+ if (existingAssociation) {
115
+ this.logger.debug(`Key ${subject.id} already bound to role ${existingRole.id}`);
116
+ continue;
117
+ }
118
+ const result = await this.iamClient.postApiV1RoleAssociationsKeyByKeyId(subject.id, { roleId: existingRole.id }, {
119
+ headers: {
120
+ Authorization: `Bearer ${await this.getToken()}`,
121
+ },
122
+ });
123
+ if (result.status !== 200) {
124
+ this.logger.error(`Failed to bind key ${subject.id} to role ${existingRole.id}: ${result.statusText}`);
125
+ }
126
+ changed = true;
127
+ break;
128
+ }
129
+ }
130
+ }
131
+ return changed;
132
+ }
133
+ catch (error) {
134
+ if (typeof error === "object" && error !== null && "error" in error) {
135
+ const err = error;
136
+ this.logger.fatal(`Failed to create role binding with error(${err.error.status} - ${err.error.code}): ${err.error.message}`);
137
+ }
138
+ else {
139
+ this.logger.fatal(`Failed to create role binding with unknown error: ${error}`);
140
+ }
141
+ }
142
+ }
143
+ async deleteRoleBinding(organizationId, roleBinding) {
144
+ const parsedRoleBinding = roleBindingDto.parse(roleBinding);
145
+ try {
146
+ const roles = await this.iamClient.getApiV1RoleAssociationsOrganizationByOrganizationId(organizationId, {
147
+ headers: {
148
+ Authorization: `Bearer ${await this.getToken()}`,
149
+ },
150
+ });
151
+ const targetRole = roles.data.find((r) => r.name === parsedRoleBinding.spec.role);
152
+ if (!targetRole) {
153
+ this.logger.fatal(`Role ${parsedRoleBinding.spec.role} not found`);
154
+ }
155
+ const associations = await this.iamClient.getApiV1RoleAssociationsByRoleId(targetRole.id, {
156
+ headers: {
157
+ Authorization: `Bearer ${await this.getToken()}`,
158
+ },
159
+ });
160
+ for (const subject of parsedRoleBinding.spec.subjects) {
161
+ switch (subject.type) {
162
+ case "user": {
163
+ const existingAssociation = associations.data.users.find((a) => a.userId === subject.id);
164
+ if (!existingAssociation) {
165
+ this.logger.error(`User ${subject.id} not bound to role ${targetRole.id}`);
166
+ continue;
167
+ }
168
+ const result = await this.iamClient.deleteApiV1RoleAssociationsUserByUserId(subject.id, { roleId: targetRole.id }, {
169
+ headers: {
170
+ Authorization: `Bearer ${await this.getToken()}`,
171
+ },
172
+ });
173
+ if (result.status !== 200) {
174
+ this.logger.error(`Failed to delete role binding for user ${subject.id}: ${result.statusText}`);
175
+ }
176
+ break;
177
+ }
178
+ case "key": {
179
+ const result = await this.iamClient.deleteApiV1RoleAssociationsKeyByKeyId(subject.id, { roleId: targetRole.id }, {
180
+ headers: {
181
+ Authorization: `Bearer ${await this.getToken()}`,
182
+ },
183
+ });
184
+ if (result.status !== 200) {
185
+ this.logger.error(`Failed to delete role binding for key ${subject.id}: ${result.statusText}`);
186
+ }
187
+ break;
188
+ }
189
+ }
190
+ }
191
+ return true;
192
+ }
193
+ catch (error) {
194
+ if (typeof error === "object" && error !== null && "error" in error) {
195
+ const err = error;
196
+ this.logger.fatal(`Failed to delete role with error(${err.error.status} - ${err.error.code}): ${err.error.message}`);
197
+ }
198
+ else {
199
+ this.logger.fatal(`Failed to delete role with unknown error: ${error}`);
200
+ }
201
+ }
202
+ }
203
+ }
@@ -0,0 +1,65 @@
1
+ import type { Logger } from "@flowcore/cli-plugin-config";
2
+ import { z } from "zod";
3
+ import type { Api as IamApi } from "../utils/clients/iam/Api.js";
4
+ export declare const roleDto: z.ZodObject<z.objectUtil.extendShape<{
5
+ apiVersion: z.ZodString;
6
+ kind: z.ZodString;
7
+ metadata: z.ZodObject<{
8
+ name: z.ZodString;
9
+ tenant: z.ZodString;
10
+ }, "strip", z.ZodTypeAny, {
11
+ name: string;
12
+ tenant: string;
13
+ }, {
14
+ name: string;
15
+ tenant: string;
16
+ }>;
17
+ }, {
18
+ spec: z.ZodObject<{
19
+ description: z.ZodOptional<z.ZodString>;
20
+ flowcoreManaged: z.ZodOptional<z.ZodBoolean>;
21
+ policies: z.ZodArray<z.ZodString, "many">;
22
+ }, "strip", z.ZodTypeAny, {
23
+ policies: string[];
24
+ description?: string | undefined;
25
+ flowcoreManaged?: boolean | undefined;
26
+ }, {
27
+ policies: string[];
28
+ description?: string | undefined;
29
+ flowcoreManaged?: boolean | undefined;
30
+ }>;
31
+ }>, "strip", z.ZodTypeAny, {
32
+ kind: string;
33
+ apiVersion: string;
34
+ metadata: {
35
+ name: string;
36
+ tenant: string;
37
+ };
38
+ spec: {
39
+ policies: string[];
40
+ description?: string | undefined;
41
+ flowcoreManaged?: boolean | undefined;
42
+ };
43
+ }, {
44
+ kind: string;
45
+ apiVersion: string;
46
+ metadata: {
47
+ name: string;
48
+ tenant: string;
49
+ };
50
+ spec: {
51
+ policies: string[];
52
+ description?: string | undefined;
53
+ flowcoreManaged?: boolean | undefined;
54
+ };
55
+ }>;
56
+ export type Role = z.infer<typeof roleDto>;
57
+ export type ApiRoleResource = Awaited<ReturnType<IamApi["getApiV1RoleAssociationsOrganizationByOrganizationId"]>>["data"][0];
58
+ export declare class RoleService {
59
+ private readonly iamClient;
60
+ private readonly logger;
61
+ private readonly getToken;
62
+ constructor(iamClient: IamApi, logger: Logger, getToken: () => Promise<string>);
63
+ createNewRole(organizationId: string, role: unknown, skipConfirmation: boolean): Promise<boolean>;
64
+ deleteRole(organizationId: string, role: unknown): Promise<boolean>;
65
+ }