@flowcore/cli-plugin-iam 1.6.1 → 1.8.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.
Files changed (47) hide show
  1. package/README.md +396 -7
  2. package/bin/dev.js +2 -2
  3. package/bin/run.js +2 -2
  4. package/dist/commands/assign/policy.d.ts +16 -0
  5. package/dist/commands/assign/policy.js +124 -0
  6. package/dist/commands/assign/role.d.ts +15 -0
  7. package/dist/commands/assign/role.js +98 -0
  8. package/dist/commands/create/policy.d.ts +16 -0
  9. package/dist/commands/create/policy.js +110 -0
  10. package/dist/commands/create/role.d.ts +14 -0
  11. package/dist/commands/create/role.js +78 -0
  12. package/dist/commands/edit/policy.js +3 -3
  13. package/dist/commands/edit/role.js +3 -3
  14. package/dist/commands/get/key-policies.d.ts +13 -0
  15. package/dist/commands/get/key-policies.js +79 -0
  16. package/dist/commands/get/key-roles.d.ts +13 -0
  17. package/dist/commands/get/key-roles.js +75 -0
  18. package/dist/commands/get/user-policies.d.ts +14 -0
  19. package/dist/commands/get/user-policies.js +94 -0
  20. package/dist/commands/get/user-roles.d.ts +14 -0
  21. package/dist/commands/get/user-roles.js +90 -0
  22. package/dist/commands/unassign/policy.d.ts +17 -0
  23. package/dist/commands/unassign/policy.js +143 -0
  24. package/dist/commands/unassign/role.d.ts +16 -0
  25. package/dist/commands/unassign/role.js +117 -0
  26. package/dist/commands/validate/key.d.ts +15 -0
  27. package/dist/commands/validate/key.js +106 -0
  28. package/dist/commands/validate/user.d.ts +15 -0
  29. package/dist/commands/validate/user.js +106 -0
  30. package/dist/index.d.ts +1 -1
  31. package/dist/index.js +1 -1
  32. package/dist/resource-types/iam-api-version.js +2 -2
  33. package/dist/resource-types/policy.resource.d.ts +8 -7
  34. package/dist/resource-types/policy.resource.js +7 -4
  35. package/dist/resource-types/role-binding.resource.d.ts +4 -4
  36. package/dist/resource-types/role.resource.d.ts +3 -3
  37. package/dist/resource-types/role.resource.js +2 -2
  38. package/dist/utils/combine-merge.util.d.ts +1 -1
  39. package/dist/utils/combine-merge.util.js +1 -1
  40. package/dist/utils/error-message.util.d.ts +1 -0
  41. package/dist/utils/error-message.util.js +4 -0
  42. package/dist/utils/fetch-manifest.util.js +2 -2
  43. package/dist/utils/read-pipe.util.js +5 -5
  44. package/oclif.manifest.json +964 -69
  45. package/package.json +18 -11
  46. package/.npmrc +0 -1
  47. package/CHANGELOG.md +0 -124
@@ -0,0 +1,90 @@
1
+ import { BaseCommand, ValidateLogin } from "@flowcore/cli-plugin-config";
2
+ import { ClientFactory } from "@flowcore/cli-plugin-core";
3
+ import { Args, Flags } from "@oclif/core";
4
+ import { tryit } from "radash";
5
+ import { OrganizationService } from "../../services/organization.service.js";
6
+ import { Api as IamApi } from "../../utils/clients/iam/Api.js";
7
+ import { getErrorMessage } from "../../utils/error-message.util.js";
8
+ export default class GetUserRoles extends BaseCommand {
9
+ static args = {
10
+ USER_ID: Args.string({
11
+ description: "The user ID to get roles for (e.g. auth0|abc123)",
12
+ required: true,
13
+ }),
14
+ };
15
+ static description = "List all IAM roles assigned to a specific user, optionally scoped to a tenant";
16
+ static examples = [
17
+ `$ flowcore iam get user-roles "auth0|abc123" -t my-org`,
18
+ `$ flowcore iam get user-roles "auth0|abc123" -j`,
19
+ `$ flowcore iam get user-roles "auth0|abc123" -t my-org -w`,
20
+ ];
21
+ static flags = {
22
+ json: Flags.boolean({
23
+ char: "j",
24
+ description: "Output result as JSON",
25
+ required: false,
26
+ }),
27
+ tenant: Flags.string({
28
+ char: "t",
29
+ description: "Scope results to a specific tenant (organization slug)",
30
+ required: false,
31
+ }),
32
+ wide: Flags.boolean({
33
+ char: "w",
34
+ description: "Show additional columns in table output",
35
+ required: false,
36
+ }),
37
+ };
38
+ async run() {
39
+ const { args, flags } = await this.parse(GetUserRoles);
40
+ const graphqlClient = await ClientFactory.create(this.cliConfiguration, this.logger, flags.json);
41
+ const organizationService = new OrganizationService(graphqlClient);
42
+ const organizations = await organizationService.getMyOrganizations();
43
+ const iamClient = new IamApi();
44
+ const config = this.cliConfiguration.getConfig();
45
+ const login = new ValidateLogin(config.login.url);
46
+ await login.validate(config, this.cliConfiguration, !flags.json);
47
+ const { auth } = config;
48
+ if (!auth?.accessToken) {
49
+ this.logger.fatal("Not logged in, run 'flowcore login'");
50
+ }
51
+ const authHeaders = {
52
+ headers: { Authorization: `Bearer ${auth?.accessToken}` },
53
+ };
54
+ let organizationId;
55
+ if (flags.tenant) {
56
+ const organization = organizations.me.organizations.find((org) => org.organization.org === flags.tenant);
57
+ if (!organization) {
58
+ this.logger.fatal(`Organization ${flags.tenant} not found, or you are not a member`);
59
+ }
60
+ organizationId = organization.organization.id;
61
+ }
62
+ const [err, response] = await tryit(iamClient.getApiV1RoleAssociationsUserByUserId)(args.USER_ID, organizationId ? { organizationId } : undefined, authHeaders);
63
+ if (err) {
64
+ this.logger.fatal(`Failed to get user roles: ${getErrorMessage(err)}`);
65
+ }
66
+ const roles = response.data;
67
+ if (flags.json) {
68
+ console.log(JSON.stringify(roles, null, 2));
69
+ }
70
+ else {
71
+ const headers = [
72
+ "Role ID",
73
+ "Name",
74
+ "Description",
75
+ ...(flags.wide ? ["Organization ID"] : []),
76
+ ];
77
+ let table = this.ui.table().head(headers);
78
+ for (const role of roles) {
79
+ const row = [
80
+ role.id,
81
+ role.name,
82
+ role.description ?? "",
83
+ ...(flags.wide ? [role.organizationId] : []),
84
+ ];
85
+ table = table.row(row);
86
+ }
87
+ table.render();
88
+ }
89
+ }
90
+ }
@@ -0,0 +1,17 @@
1
+ import { BaseCommand } from "@flowcore/cli-plugin-config";
2
+ export default class UnassignPolicy extends BaseCommand<typeof UnassignPolicy> {
3
+ static args: {
4
+ POLICY_NAME: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ key: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ role: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ tenant: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
13
+ user: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
+ };
16
+ run(): Promise<void>;
17
+ }
@@ -0,0 +1,143 @@
1
+ import { BaseCommand, ValidateLogin } from "@flowcore/cli-plugin-config";
2
+ import { ClientFactory } from "@flowcore/cli-plugin-core";
3
+ import { Args, Flags, ux } from "@oclif/core";
4
+ import enquirer from "enquirer";
5
+ import { tryit } from "radash";
6
+ import { OrganizationService } from "../../services/organization.service.js";
7
+ import { Api as IamApi } from "../../utils/clients/iam/Api.js";
8
+ import { getErrorMessage } from "../../utils/error-message.util.js";
9
+ export default class UnassignPolicy extends BaseCommand {
10
+ static args = {
11
+ POLICY_NAME: Args.string({
12
+ description: "The name of the policy to unassign",
13
+ required: true,
14
+ }),
15
+ };
16
+ static description = "Remove an IAM policy assignment from a user, API key, or role. Exactly one of --user, --key, or --role must be specified";
17
+ static examples = [
18
+ `$ flowcore iam unassign policy read-access --user "auth0|abc123" -t my-org -y`,
19
+ `$ flowcore iam unassign policy read-access --key "550e8400-e29b-41d4-a716-446655440000" -t my-org -y`,
20
+ "$ flowcore iam unassign policy read-access --role data-reader -t my-org -y",
21
+ `$ flowcore iam unassign policy read-access --user "auth0|abc123" -t my-org -j -y`,
22
+ ];
23
+ static flags = {
24
+ json: Flags.boolean({
25
+ char: "j",
26
+ description: "Output result as JSON",
27
+ required: false,
28
+ }),
29
+ key: Flags.string({
30
+ description: "The API key ID to unassign the policy from",
31
+ exclusive: ["user", "role"],
32
+ required: false,
33
+ }),
34
+ role: Flags.string({
35
+ description: "The role name to unassign the policy from",
36
+ exclusive: ["user", "key"],
37
+ required: false,
38
+ }),
39
+ tenant: Flags.string({
40
+ char: "t",
41
+ description: "The tenant (organization slug) containing the policy",
42
+ required: true,
43
+ }),
44
+ user: Flags.string({
45
+ description: "The user ID to unassign the policy from",
46
+ exclusive: ["key", "role"],
47
+ required: false,
48
+ }),
49
+ yes: Flags.boolean({
50
+ char: "y",
51
+ description: "Skip confirmation prompt",
52
+ required: false,
53
+ }),
54
+ };
55
+ async run() {
56
+ const { args, flags } = await this.parse(UnassignPolicy);
57
+ if (!flags.user && !flags.key && !flags.role) {
58
+ this.logger.fatal("Exactly one of --user, --key, or --role is required");
59
+ }
60
+ const graphqlClient = await ClientFactory.create(this.cliConfiguration, this.logger, flags.json);
61
+ const organizationService = new OrganizationService(graphqlClient);
62
+ const organizations = await organizationService.getMyOrganizations();
63
+ const iamClient = new IamApi();
64
+ const config = this.cliConfiguration.getConfig();
65
+ const login = new ValidateLogin(config.login.url);
66
+ await login.validate(config, this.cliConfiguration, !flags.json);
67
+ const { auth } = config;
68
+ if (!auth?.accessToken) {
69
+ this.logger.fatal("Not logged in, run 'flowcore login'");
70
+ }
71
+ const organization = organizations.me.organizations.find((org) => org.organization.org === flags.tenant);
72
+ if (!organization) {
73
+ this.logger.fatal(`Organization ${flags.tenant} not found, or you are not a member`);
74
+ }
75
+ const authHeaders = {
76
+ headers: { Authorization: `Bearer ${auth?.accessToken}` },
77
+ };
78
+ // Resolve policy name to ID
79
+ const [policiesErr, policies] = await tryit(iamClient.getApiV1PolicyAssociationsOrganizationByOrganizationId)(organization.organization.id, authHeaders);
80
+ if (policiesErr) {
81
+ this.logger.fatal(`Failed to get policies: ${policiesErr.message}`);
82
+ }
83
+ const policy = policies.data.find((p) => p.name === args.POLICY_NAME);
84
+ if (!policy) {
85
+ this.logger.fatal(`Policy "${args.POLICY_NAME}" not found in tenant: ${flags.tenant}`);
86
+ }
87
+ const target = flags.user
88
+ ? `user ${flags.user}`
89
+ : flags.key
90
+ ? `key ${flags.key}`
91
+ : `role ${flags.role}`;
92
+ if (!flags.yes) {
93
+ const { confirm } = await enquirer.prompt([
94
+ {
95
+ message: `Are you sure you want to unassign policy ${ux.colorize("cyan", args.POLICY_NAME)} from ${ux.colorize("cyan", target)}?`,
96
+ name: "confirm",
97
+ type: "confirm",
98
+ },
99
+ ]);
100
+ if (!confirm) {
101
+ this.logger.info("Aborted");
102
+ return;
103
+ }
104
+ }
105
+ let result;
106
+ if (flags.user) {
107
+ const [err, response] = await tryit(iamClient.deleteApiV1PolicyAssociationsUserByUserId)(flags.user, { policyId: policy.id }, authHeaders);
108
+ if (err) {
109
+ this.logger.fatal(`Failed to unassign policy from user: ${getErrorMessage(err)}`);
110
+ }
111
+ result = response.data;
112
+ }
113
+ else if (flags.key) {
114
+ const [err, response] = await tryit(iamClient.deleteApiV1PolicyAssociationsKeyByKeyId)(flags.key, { policyId: policy.id }, authHeaders);
115
+ if (err) {
116
+ this.logger.fatal(`Failed to unassign policy from key: ${getErrorMessage(err)}`);
117
+ }
118
+ result = response.data;
119
+ }
120
+ else if (flags.role) {
121
+ // Resolve role name to ID
122
+ const [rolesErr, roles] = await tryit(iamClient.getApiV1RoleAssociationsOrganizationByOrganizationId)(organization.organization.id, authHeaders);
123
+ if (rolesErr) {
124
+ this.logger.fatal(`Failed to get roles: ${rolesErr.message}`);
125
+ }
126
+ const role = roles.data.find((r) => r.name === flags.role);
127
+ if (!role) {
128
+ this.logger.fatal(`Role "${flags.role}" not found in tenant: ${flags.tenant}`);
129
+ }
130
+ const [err, response] = await tryit(iamClient.deleteApiV1PolicyAssociationsRoleByRoleId)(role.id, { policyId: policy.id }, authHeaders);
131
+ if (err) {
132
+ this.logger.fatal(`Failed to unassign policy from role: ${getErrorMessage(err)}`);
133
+ }
134
+ result = response.data;
135
+ }
136
+ if (flags.json) {
137
+ console.log(JSON.stringify(result, null, 2));
138
+ }
139
+ else {
140
+ this.logger.info(`Policy "${args.POLICY_NAME}" unassigned from ${target}`);
141
+ }
142
+ }
143
+ }
@@ -0,0 +1,16 @@
1
+ import { BaseCommand } from "@flowcore/cli-plugin-config";
2
+ export default class UnassignRole extends BaseCommand<typeof UnassignRole> {
3
+ static args: {
4
+ ROLE_NAME: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ key: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ tenant: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
12
+ user: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
+ };
15
+ run(): Promise<void>;
16
+ }
@@ -0,0 +1,117 @@
1
+ import { BaseCommand, ValidateLogin } from "@flowcore/cli-plugin-config";
2
+ import { ClientFactory } from "@flowcore/cli-plugin-core";
3
+ import { Args, Flags, ux } from "@oclif/core";
4
+ import enquirer from "enquirer";
5
+ import { tryit } from "radash";
6
+ import { OrganizationService } from "../../services/organization.service.js";
7
+ import { Api as IamApi } from "../../utils/clients/iam/Api.js";
8
+ import { getErrorMessage } from "../../utils/error-message.util.js";
9
+ export default class UnassignRole extends BaseCommand {
10
+ static args = {
11
+ ROLE_NAME: Args.string({
12
+ description: "The name of the role to unassign",
13
+ required: true,
14
+ }),
15
+ };
16
+ static description = "Remove an IAM role assignment from a user or API key. Exactly one of --user or --key must be specified";
17
+ static examples = [
18
+ `$ flowcore iam unassign role data-reader --user "auth0|abc123" -t my-org -y`,
19
+ `$ flowcore iam unassign role data-reader --key "550e8400-e29b-41d4-a716-446655440000" -t my-org -y`,
20
+ `$ flowcore iam unassign role data-reader --user "auth0|abc123" -t my-org -j -y`,
21
+ ];
22
+ static flags = {
23
+ json: Flags.boolean({
24
+ char: "j",
25
+ description: "Output result as JSON",
26
+ required: false,
27
+ }),
28
+ key: Flags.string({
29
+ description: "The API key ID to unassign the role from",
30
+ exclusive: ["user"],
31
+ required: false,
32
+ }),
33
+ tenant: Flags.string({
34
+ char: "t",
35
+ description: "The tenant (organization slug) containing the role",
36
+ required: true,
37
+ }),
38
+ user: Flags.string({
39
+ description: "The user ID to unassign the role from",
40
+ exclusive: ["key"],
41
+ required: false,
42
+ }),
43
+ yes: Flags.boolean({
44
+ char: "y",
45
+ description: "Skip confirmation prompt",
46
+ required: false,
47
+ }),
48
+ };
49
+ async run() {
50
+ const { args, flags } = await this.parse(UnassignRole);
51
+ if (!flags.user && !flags.key) {
52
+ this.logger.fatal("Exactly one of --user or --key is required");
53
+ }
54
+ const graphqlClient = await ClientFactory.create(this.cliConfiguration, this.logger, flags.json);
55
+ const organizationService = new OrganizationService(graphqlClient);
56
+ const organizations = await organizationService.getMyOrganizations();
57
+ const iamClient = new IamApi();
58
+ const config = this.cliConfiguration.getConfig();
59
+ const login = new ValidateLogin(config.login.url);
60
+ await login.validate(config, this.cliConfiguration, !flags.json);
61
+ const { auth } = config;
62
+ if (!auth?.accessToken) {
63
+ this.logger.fatal("Not logged in, run 'flowcore login'");
64
+ }
65
+ const organization = organizations.me.organizations.find((org) => org.organization.org === flags.tenant);
66
+ if (!organization) {
67
+ this.logger.fatal(`Organization ${flags.tenant} not found, or you are not a member`);
68
+ }
69
+ const authHeaders = {
70
+ headers: { Authorization: `Bearer ${auth?.accessToken}` },
71
+ };
72
+ // Resolve role name to ID
73
+ const [rolesErr, roles] = await tryit(iamClient.getApiV1RoleAssociationsOrganizationByOrganizationId)(organization.organization.id, authHeaders);
74
+ if (rolesErr) {
75
+ this.logger.fatal(`Failed to get roles: ${rolesErr.message}`);
76
+ }
77
+ const role = roles.data.find((r) => r.name === args.ROLE_NAME);
78
+ if (!role) {
79
+ this.logger.fatal(`Role "${args.ROLE_NAME}" not found in tenant: ${flags.tenant}`);
80
+ }
81
+ const target = flags.user ? `user ${flags.user}` : `key ${flags.key}`;
82
+ if (!flags.yes) {
83
+ const { confirm } = await enquirer.prompt([
84
+ {
85
+ message: `Are you sure you want to unassign role ${ux.colorize("cyan", args.ROLE_NAME)} from ${ux.colorize("cyan", target)}?`,
86
+ name: "confirm",
87
+ type: "confirm",
88
+ },
89
+ ]);
90
+ if (!confirm) {
91
+ this.logger.info("Aborted");
92
+ return;
93
+ }
94
+ }
95
+ let result;
96
+ if (flags.user) {
97
+ const [err, response] = await tryit(iamClient.deleteApiV1RoleAssociationsUserByUserId)(flags.user, { roleId: role.id }, authHeaders);
98
+ if (err) {
99
+ this.logger.fatal(`Failed to unassign role from user: ${getErrorMessage(err)}`);
100
+ }
101
+ result = response.data;
102
+ }
103
+ else if (flags.key) {
104
+ const [err, response] = await tryit(iamClient.deleteApiV1RoleAssociationsKeyByKeyId)(flags.key, { roleId: role.id }, authHeaders);
105
+ if (err) {
106
+ this.logger.fatal(`Failed to unassign role from key: ${getErrorMessage(err)}`);
107
+ }
108
+ result = response.data;
109
+ }
110
+ if (flags.json) {
111
+ console.log(JSON.stringify(result, null, 2));
112
+ }
113
+ else {
114
+ this.logger.info(`Role "${args.ROLE_NAME}" unassigned from ${target}`);
115
+ }
116
+ }
117
+ }
@@ -0,0 +1,15 @@
1
+ import { BaseCommand } from "@flowcore/cli-plugin-config";
2
+ export default class ValidateKey extends BaseCommand<typeof ValidateKey> {
3
+ static args: {
4
+ KEY_ID: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ action: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ resource: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
12
+ tenant: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
13
+ };
14
+ run(): Promise<void>;
15
+ }
@@ -0,0 +1,106 @@
1
+ import { BaseCommand, ValidateLogin } from "@flowcore/cli-plugin-config";
2
+ import { ClientFactory } from "@flowcore/cli-plugin-core";
3
+ import { Args, Flags } from "@oclif/core";
4
+ import { tryit } from "radash";
5
+ import { OrganizationService } from "../../services/organization.service.js";
6
+ import { Api as IamApi } from "../../utils/clients/iam/Api.js";
7
+ import { getErrorMessage } from "../../utils/error-message.util.js";
8
+ export default class ValidateKey extends BaseCommand {
9
+ static args = {
10
+ KEY_ID: Args.string({
11
+ description: "The API key ID to validate access for",
12
+ required: true,
13
+ }),
14
+ };
15
+ static description = "Validate whether an API key has permission to perform an action on one or more resources";
16
+ static examples = [
17
+ `$ flowcore iam validate key "550e8400-e29b-41d4-a716-446655440000" -t my-org --action ingest --resource "frn::my-org:event-type/*"`,
18
+ `$ flowcore iam validate key "550e8400-e29b-41d4-a716-446655440000" -t my-org --action read --resource "frn::my-org:data-core/my-core" -j`,
19
+ `$ flowcore iam validate key "550e8400-e29b-41d4-a716-446655440000" -t my-org --action read --resource "frn::my-org:data-core/core1" --resource "frn::my-org:data-core/core2"`,
20
+ ];
21
+ static flags = {
22
+ action: Flags.string({
23
+ description: "The action to validate (e.g. read, write, ingest, fetch)",
24
+ required: true,
25
+ }),
26
+ json: Flags.boolean({
27
+ char: "j",
28
+ description: "Output result as JSON",
29
+ required: false,
30
+ }),
31
+ resource: Flags.string({
32
+ description: "The resource FRN to validate against (can be specified multiple times)",
33
+ multiple: true,
34
+ required: true,
35
+ }),
36
+ tenant: Flags.string({
37
+ char: "t",
38
+ description: "The tenant (organization slug) to validate within",
39
+ required: true,
40
+ }),
41
+ };
42
+ async run() {
43
+ const { args, flags } = await this.parse(ValidateKey);
44
+ const graphqlClient = await ClientFactory.create(this.cliConfiguration, this.logger, flags.json);
45
+ const organizationService = new OrganizationService(graphqlClient);
46
+ const organizations = await organizationService.getMyOrganizations();
47
+ const iamClient = new IamApi();
48
+ const config = this.cliConfiguration.getConfig();
49
+ const login = new ValidateLogin(config.login.url);
50
+ await login.validate(config, this.cliConfiguration, !flags.json);
51
+ const { auth } = config;
52
+ if (!auth?.accessToken) {
53
+ this.logger.fatal("Not logged in, run 'flowcore login'");
54
+ }
55
+ const organization = organizations.me.organizations.find((org) => org.organization.org === flags.tenant);
56
+ if (!organization) {
57
+ this.logger.fatal(`Organization ${flags.tenant} not found, or you are not a member`);
58
+ }
59
+ const [err, response] = await tryit(iamClient.postApiV1ValidateKeysById)(args.KEY_ID, {
60
+ mode: "tenant",
61
+ requestedAccess: [
62
+ {
63
+ action: flags.action,
64
+ resource: flags.resource,
65
+ },
66
+ ],
67
+ }, {
68
+ headers: {
69
+ Authorization: `Bearer ${auth?.accessToken}`,
70
+ },
71
+ });
72
+ if (err) {
73
+ // 403 responses contain validation failure details in the error body
74
+ const errorResponse = err;
75
+ if (errorResponse.error && errorResponse.error.valid === false) {
76
+ if (flags.json) {
77
+ console.log(JSON.stringify(errorResponse.error, null, 2));
78
+ }
79
+ else {
80
+ this.logger.info(`INVALID \u2717 — ${errorResponse.error.message}`);
81
+ if (errorResponse.error.invalidRequest &&
82
+ Array.isArray(errorResponse.error.invalidRequest)) {
83
+ for (const req of errorResponse.error.invalidRequest) {
84
+ const r = req;
85
+ this.logger.info(` Action: ${String(r.action)}, Resources: ${r.resource.join(", ")}`);
86
+ }
87
+ }
88
+ }
89
+ return;
90
+ }
91
+ this.logger.fatal(`Failed to validate key access: ${getErrorMessage(err)}`);
92
+ }
93
+ if (flags.json) {
94
+ console.log(JSON.stringify(response.data, null, 2));
95
+ }
96
+ else {
97
+ this.logger.info("VALID \u2713");
98
+ if (response.data.validPolicies.length > 0) {
99
+ this.logger.info("Matching policies:");
100
+ for (const p of response.data.validPolicies) {
101
+ this.logger.info(` Policy FRN: ${p.policyFrn}, Statement: ${p.statementId}`);
102
+ }
103
+ }
104
+ }
105
+ }
106
+ }
@@ -0,0 +1,15 @@
1
+ import { BaseCommand } from "@flowcore/cli-plugin-config";
2
+ export default class ValidateUser extends BaseCommand<typeof ValidateUser> {
3
+ static args: {
4
+ USER_ID: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ action: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ resource: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
12
+ tenant: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
13
+ };
14
+ run(): Promise<void>;
15
+ }
@@ -0,0 +1,106 @@
1
+ import { BaseCommand, ValidateLogin } from "@flowcore/cli-plugin-config";
2
+ import { ClientFactory } from "@flowcore/cli-plugin-core";
3
+ import { Args, Flags } from "@oclif/core";
4
+ import { tryit } from "radash";
5
+ import { OrganizationService } from "../../services/organization.service.js";
6
+ import { Api as IamApi } from "../../utils/clients/iam/Api.js";
7
+ import { getErrorMessage } from "../../utils/error-message.util.js";
8
+ export default class ValidateUser extends BaseCommand {
9
+ static args = {
10
+ USER_ID: Args.string({
11
+ description: "The user ID to validate access for (e.g. auth0|abc123)",
12
+ required: true,
13
+ }),
14
+ };
15
+ static description = "Validate whether a user has permission to perform an action on one or more resources";
16
+ static examples = [
17
+ `$ flowcore iam validate user "auth0|abc123" -t my-org --action read --resource "frn::my-org:data-core/my-core"`,
18
+ `$ flowcore iam validate user "auth0|abc123" -t my-org --action write --resource "frn::my-org:data-core/*" -j`,
19
+ `$ flowcore iam validate user "auth0|abc123" -t my-org --action read --resource "frn::my-org:data-core/core1" --resource "frn::my-org:data-core/core2"`,
20
+ ];
21
+ static flags = {
22
+ action: Flags.string({
23
+ description: "The action to validate (e.g. read, write, ingest, fetch)",
24
+ required: true,
25
+ }),
26
+ json: Flags.boolean({
27
+ char: "j",
28
+ description: "Output result as JSON",
29
+ required: false,
30
+ }),
31
+ resource: Flags.string({
32
+ description: "The resource FRN to validate against (can be specified multiple times)",
33
+ multiple: true,
34
+ required: true,
35
+ }),
36
+ tenant: Flags.string({
37
+ char: "t",
38
+ description: "The tenant (organization slug) to validate within",
39
+ required: true,
40
+ }),
41
+ };
42
+ async run() {
43
+ const { args, flags } = await this.parse(ValidateUser);
44
+ const graphqlClient = await ClientFactory.create(this.cliConfiguration, this.logger, flags.json);
45
+ const organizationService = new OrganizationService(graphqlClient);
46
+ const organizations = await organizationService.getMyOrganizations();
47
+ const iamClient = new IamApi();
48
+ const config = this.cliConfiguration.getConfig();
49
+ const login = new ValidateLogin(config.login.url);
50
+ await login.validate(config, this.cliConfiguration, !flags.json);
51
+ const { auth } = config;
52
+ if (!auth?.accessToken) {
53
+ this.logger.fatal("Not logged in, run 'flowcore login'");
54
+ }
55
+ const organization = organizations.me.organizations.find((org) => org.organization.org === flags.tenant);
56
+ if (!organization) {
57
+ this.logger.fatal(`Organization ${flags.tenant} not found, or you are not a member`);
58
+ }
59
+ const [err, response] = await tryit(iamClient.postApiV1ValidateUsersById)(args.USER_ID, {
60
+ mode: "tenant",
61
+ requestedAccess: [
62
+ {
63
+ action: flags.action,
64
+ resource: flags.resource,
65
+ },
66
+ ],
67
+ }, {
68
+ headers: {
69
+ Authorization: `Bearer ${auth?.accessToken}`,
70
+ },
71
+ });
72
+ if (err) {
73
+ // 403 responses contain validation failure details in the error body
74
+ const errorResponse = err;
75
+ if (errorResponse.error && errorResponse.error.valid === false) {
76
+ if (flags.json) {
77
+ console.log(JSON.stringify(errorResponse.error, null, 2));
78
+ }
79
+ else {
80
+ this.logger.info(`INVALID \u2717 — ${errorResponse.error.message}`);
81
+ if (errorResponse.error.invalidRequest &&
82
+ Array.isArray(errorResponse.error.invalidRequest)) {
83
+ for (const req of errorResponse.error.invalidRequest) {
84
+ const r = req;
85
+ this.logger.info(` Action: ${String(r.action)}, Resources: ${r.resource.join(", ")}`);
86
+ }
87
+ }
88
+ }
89
+ return;
90
+ }
91
+ this.logger.fatal(`Failed to validate user access: ${getErrorMessage(err)}`);
92
+ }
93
+ if (flags.json) {
94
+ console.log(JSON.stringify(response.data, null, 2));
95
+ }
96
+ else {
97
+ this.logger.info("VALID \u2713");
98
+ if (response.data.validPolicies.length > 0) {
99
+ this.logger.info("Matching policies:");
100
+ for (const p of response.data.validPolicies) {
101
+ this.logger.info(` Policy FRN: ${p.policyFrn}, Statement: ${p.statementId}`);
102
+ }
103
+ }
104
+ }
105
+ }
106
+ }
package/dist/index.d.ts CHANGED
@@ -1 +1 @@
1
- export { run } from '@oclif/core';
1
+ export { run } from "@oclif/core";
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export { run } from '@oclif/core';
1
+ export { run } from "@oclif/core";
@@ -1,9 +1,9 @@
1
1
  import { ValidateLogin, } from "@flowcore/cli-plugin-config";
2
2
  import { Api as IamApi } from "../utils/clients/iam/Api.js";
3
- import { PolicyService } from "./policy.resource.js";
4
3
  import { PolicyBindingService } from "./policy-binding.resource.js";
5
- import { RoleService } from "./role.resource.js";
4
+ import { PolicyService } from "./policy.resource.js";
6
5
  import { RoleBindingService } from "./role-binding.resource.js";
6
+ import { RoleService } from "./role.resource.js";
7
7
  const applyOrder = ["Policy", "PolicyBinding", "Role", "RoleBinding"];
8
8
  export class IAMApply {
9
9
  organizationService;