@credibledata/cred-cli 0.0.102 → 0.0.103

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 (118) hide show
  1. package/README.md +1 -1
  2. package/coverage/coverage-summary.json +40 -0
  3. package/coverage/tmp/coverage-2152-1789496974457-0.json +1 -0
  4. package/coverage/tmp/coverage-2153-1789496954404-0.json +1 -0
  5. package/dist/client/api.js +4193 -1342
  6. package/dist/client/api.js.map +1 -1
  7. package/dist/clientconfiguration.js +57 -4
  8. package/dist/clientconfiguration.js.map +1 -1
  9. package/dist/clientconfiguration.spec.js +90 -0
  10. package/dist/clientconfiguration.spec.js.map +1 -1
  11. package/dist/commands/add.js +18 -0
  12. package/dist/commands/add.js.map +1 -1
  13. package/dist/commands/environment/updateenvironment.js +63 -1
  14. package/dist/commands/environment/updateenvironment.js.map +1 -1
  15. package/dist/commands/environment/updateenvironment.spec.js +84 -0
  16. package/dist/commands/environment/updateenvironment.spec.js.map +1 -1
  17. package/dist/commands/get.js +52 -1
  18. package/dist/commands/get.js.map +1 -1
  19. package/dist/commands/list.js +78 -0
  20. package/dist/commands/list.js.map +1 -1
  21. package/dist/commands/materialization/cancelrun.js +62 -0
  22. package/dist/commands/materialization/cancelrun.js.map +1 -0
  23. package/dist/commands/materialization/getindex.js +52 -0
  24. package/dist/commands/materialization/getindex.js.map +1 -0
  25. package/dist/commands/materialization/getmaterialization.js +54 -0
  26. package/dist/commands/materialization/getmaterialization.js.map +1 -0
  27. package/dist/commands/materialization/getpreaggregate.js +55 -0
  28. package/dist/commands/materialization/getpreaggregate.js.map +1 -0
  29. package/dist/commands/materialization/getrun.js +54 -0
  30. package/dist/commands/materialization/getrun.js.map +1 -0
  31. package/dist/commands/materialization/listindexes.js +58 -0
  32. package/dist/commands/materialization/listindexes.js.map +1 -0
  33. package/dist/commands/materialization/listmaterializations.js +52 -0
  34. package/dist/commands/materialization/listmaterializations.js.map +1 -0
  35. package/dist/commands/materialization/listpreaggregates.js +61 -0
  36. package/dist/commands/materialization/listpreaggregates.js.map +1 -0
  37. package/dist/commands/materialization/listruns.js +60 -0
  38. package/dist/commands/materialization/listruns.js.map +1 -0
  39. package/dist/commands/materialization/rerun.js +111 -0
  40. package/dist/commands/materialization/rerun.js.map +1 -0
  41. package/dist/commands/misc/login.js +14 -0
  42. package/dist/commands/misc/login.js.map +1 -1
  43. package/dist/commands/misc/login.spec.js +92 -0
  44. package/dist/commands/misc/login.spec.js.map +1 -1
  45. package/dist/commands/package/listpackages.js +7 -0
  46. package/dist/commands/package/listpackages.js.map +1 -1
  47. package/dist/commands/package/listpackages.spec.js +12 -0
  48. package/dist/commands/package/listpackages.spec.js.map +1 -1
  49. package/dist/commands/package/publish.js +34 -11
  50. package/dist/commands/package/publish.js.map +1 -1
  51. package/dist/commands/package/publish.spec.js +26 -28
  52. package/dist/commands/package/publish.spec.js.map +1 -1
  53. package/dist/commands/permission/createpermission.js +123 -0
  54. package/dist/commands/permission/createpermission.js.map +1 -0
  55. package/dist/commands/permission/createpermission.spec.js +360 -0
  56. package/dist/commands/permission/createpermission.spec.js.map +1 -0
  57. package/dist/commands/permission/deletepermission.js +86 -0
  58. package/dist/commands/permission/deletepermission.js.map +1 -0
  59. package/dist/commands/permission/getpermission.js +85 -0
  60. package/dist/commands/permission/getpermission.js.map +1 -0
  61. package/dist/commands/permission/listpermissions.js +75 -0
  62. package/dist/commands/permission/listpermissions.js.map +1 -0
  63. package/dist/commands/permission/scope.js +140 -0
  64. package/dist/commands/permission/scope.js.map +1 -0
  65. package/dist/commands/rm.js +11 -0
  66. package/dist/commands/rm.js.map +1 -1
  67. package/dist/commands/update.js +18 -12
  68. package/dist/commands/update.js.map +1 -1
  69. package/dist/common/UserGroupId.js +1 -1
  70. package/dist/common/UserGroupId.js.map +1 -1
  71. package/dist/common/UserGroupId.spec.js +2 -4
  72. package/dist/common/UserGroupId.spec.js.map +1 -1
  73. package/dist/constants.js +48 -0
  74. package/dist/constants.js.map +1 -1
  75. package/dist/index.js +61 -6
  76. package/dist/index.js.map +1 -1
  77. package/dist/logger.js +5 -1
  78. package/dist/logger.js.map +1 -1
  79. package/openapitools.json +4 -1
  80. package/package.json +5 -3
  81. package/src/client/api.ts +6433 -1533
  82. package/src/clientconfiguration.spec.ts +126 -0
  83. package/src/clientconfiguration.ts +64 -4
  84. package/src/commands/add.ts +43 -0
  85. package/src/commands/environment/updateenvironment.spec.ts +167 -0
  86. package/src/commands/environment/updateenvironment.ts +85 -1
  87. package/src/commands/get.ts +110 -1
  88. package/src/commands/list.ts +171 -0
  89. package/src/commands/materialization/cancelrun.ts +70 -0
  90. package/src/commands/materialization/getindex.ts +55 -0
  91. package/src/commands/materialization/getmaterialization.ts +57 -0
  92. package/src/commands/materialization/getpreaggregate.ts +58 -0
  93. package/src/commands/materialization/getrun.ts +55 -0
  94. package/src/commands/materialization/listindexes.ts +69 -0
  95. package/src/commands/materialization/listmaterializations.ts +53 -0
  96. package/src/commands/materialization/listpreaggregates.ts +74 -0
  97. package/src/commands/materialization/listruns.ts +77 -0
  98. package/src/commands/materialization/rerun.ts +123 -0
  99. package/src/commands/misc/login.spec.ts +118 -2
  100. package/src/commands/misc/login.ts +14 -0
  101. package/src/commands/package/listpackages.spec.ts +16 -0
  102. package/src/commands/package/listpackages.ts +7 -0
  103. package/src/commands/package/publish.spec.ts +45 -30
  104. package/src/commands/package/publish.ts +31 -10
  105. package/src/commands/permission/createpermission.spec.ts +507 -0
  106. package/src/commands/permission/createpermission.ts +181 -0
  107. package/src/commands/permission/deletepermission.ts +117 -0
  108. package/src/commands/permission/getpermission.ts +115 -0
  109. package/src/commands/permission/listpermissions.ts +101 -0
  110. package/src/commands/permission/scope.ts +180 -0
  111. package/src/commands/rm.ts +29 -0
  112. package/src/commands/update.ts +50 -27
  113. package/src/common/UserGroupId.spec.ts +2 -4
  114. package/src/common/UserGroupId.ts +1 -1
  115. package/src/constants.ts +50 -1
  116. package/src/index.ts +94 -11
  117. package/src/logger.ts +5 -1
  118. package/bun.lock +0 -934
@@ -0,0 +1,117 @@
1
+ import {
2
+ EnvironmentPermissionsApi,
3
+ OrganizationPermissionsApi,
4
+ PackagePermissionsApi,
5
+ } from "../../client/api";
6
+ import { Configuration } from "../../client/configuration";
7
+ import { ClientConfiguration } from "../../clientconfiguration";
8
+ import { UserGroupId } from "../../common/UserGroupId";
9
+ import { logger } from "../../logger";
10
+ import { getConfirmation, printOutError } from "../../utils";
11
+ import {
12
+ describeScope,
13
+ PermissionScope,
14
+ resolveScope,
15
+ ScopeOptions,
16
+ } from "./scope";
17
+
18
+ const organizationPermissionsApi = new OrganizationPermissionsApi(
19
+ new Configuration(),
20
+ );
21
+ const environmentPermissionsApi = new EnvironmentPermissionsApi(
22
+ new Configuration(),
23
+ );
24
+ const packagePermissionsApi = new PackagePermissionsApi(new Configuration());
25
+
26
+ export async function deletePermission(
27
+ controlPlane: string,
28
+ userGroupIdValue: string,
29
+ debug: boolean,
30
+ autoConfirm: boolean,
31
+ options: ScopeOptions,
32
+ ) {
33
+ const clientConfig = new ClientConfiguration();
34
+
35
+ if (!clientConfig.doesConfigExists()) {
36
+ logger.error("Please, make sure that you are logged in");
37
+ return;
38
+ }
39
+
40
+ const organizationName = clientConfig.getOrganization();
41
+ if (!organizationName) {
42
+ logger.error("No organization set. Please login or set an organization.");
43
+ return;
44
+ }
45
+
46
+ const resolved = resolveScope(options, clientConfig.getProject());
47
+ if ("error" in resolved) {
48
+ logger.error(resolved.error);
49
+ process.exit(1);
50
+ return;
51
+ }
52
+
53
+ let userGroupId: string;
54
+ try {
55
+ userGroupId = new UserGroupId(userGroupIdValue).getValue();
56
+ } catch (e) {
57
+ logger.error((e as Error).message);
58
+ process.exit(1);
59
+ return;
60
+ }
61
+
62
+ const target = describeScope(organizationName, resolved);
63
+
64
+ if (!autoConfirm) {
65
+ const confirmation = await getConfirmation(
66
+ `Are you sure that you want to revoke ${userGroupId}'s permission on ${target}?`,
67
+ );
68
+ if (!confirmation) {
69
+ logger.info("Operation cancelled.");
70
+ return;
71
+ }
72
+ }
73
+
74
+ logger.info(`Revoking permission for ${userGroupId} on ${target}`);
75
+
76
+ const requestOptions = {
77
+ baseURL: controlPlane,
78
+ headers: {
79
+ Authorization: clientConfig.getAuthenticationHeader(),
80
+ },
81
+ };
82
+
83
+ try {
84
+ switch (resolved.scope) {
85
+ case PermissionScope.PACKAGE:
86
+ await packagePermissionsApi.deletePackagePermission(
87
+ organizationName,
88
+ resolved.environmentName as string,
89
+ resolved.packageName as string,
90
+ userGroupId,
91
+ requestOptions,
92
+ );
93
+ break;
94
+ case PermissionScope.ENVIRONMENT:
95
+ await environmentPermissionsApi.deleteEnvironmentPermission(
96
+ organizationName,
97
+ resolved.environmentName as string,
98
+ userGroupId,
99
+ requestOptions,
100
+ );
101
+ break;
102
+ default:
103
+ await organizationPermissionsApi.deleteOrganizationPermission(
104
+ organizationName,
105
+ userGroupId,
106
+ requestOptions,
107
+ );
108
+ break;
109
+ }
110
+
111
+ logger.info("Permission revoked successfully");
112
+ } catch (error) {
113
+ logger.error("Failed to revoke permission");
114
+ printOutError(error, debug);
115
+ process.exit(1);
116
+ }
117
+ }
@@ -0,0 +1,115 @@
1
+ import {
2
+ EnvironmentPermissionsApi,
3
+ OrganizationPermissionsApi,
4
+ PackagePermissionsApi,
5
+ } from "../../client/api";
6
+ import { Configuration } from "../../client/configuration";
7
+ import { ClientConfiguration } from "../../clientconfiguration";
8
+ import { UserGroupId } from "../../common/UserGroupId";
9
+ import { logger } from "../../logger";
10
+ import { printOutError } from "../../utils";
11
+ import {
12
+ describeScope,
13
+ PermissionScope,
14
+ resolveScope,
15
+ ScopeOptions,
16
+ } from "./scope";
17
+
18
+ const organizationPermissionsApi = new OrganizationPermissionsApi(
19
+ new Configuration(),
20
+ );
21
+ const environmentPermissionsApi = new EnvironmentPermissionsApi(
22
+ new Configuration(),
23
+ );
24
+ const packagePermissionsApi = new PackagePermissionsApi(new Configuration());
25
+
26
+ export async function getPermission(
27
+ controlPlane: string,
28
+ userGroupIdValue: string,
29
+ debug: boolean,
30
+ options: ScopeOptions,
31
+ ) {
32
+ const clientConfig = new ClientConfiguration();
33
+
34
+ if (!clientConfig.doesConfigExists()) {
35
+ logger.error("Please, make sure that you are logged in");
36
+ return;
37
+ }
38
+
39
+ const organizationName = clientConfig.getOrganization();
40
+ if (!organizationName) {
41
+ logger.error("No organization set. Please login or set an organization.");
42
+ return;
43
+ }
44
+
45
+ const resolved = resolveScope(options, clientConfig.getProject());
46
+ if ("error" in resolved) {
47
+ logger.error(resolved.error);
48
+ process.exit(1);
49
+ return;
50
+ }
51
+
52
+ let userGroupId: string;
53
+ try {
54
+ userGroupId = new UserGroupId(userGroupIdValue).getValue();
55
+ } catch (e) {
56
+ logger.error((e as Error).message);
57
+ process.exit(1);
58
+ return;
59
+ }
60
+
61
+ logger.info(
62
+ `Getting permission for ${userGroupId} on ${describeScope(organizationName, resolved)}`,
63
+ );
64
+
65
+ const requestOptions = {
66
+ baseURL: controlPlane,
67
+ headers: {
68
+ Authorization: clientConfig.getAuthenticationHeader(),
69
+ },
70
+ };
71
+
72
+ try {
73
+ let data: unknown;
74
+ switch (resolved.scope) {
75
+ case PermissionScope.PACKAGE: {
76
+ const response = await packagePermissionsApi.getPackagePermission(
77
+ organizationName,
78
+ resolved.environmentName as string,
79
+ resolved.packageName as string,
80
+ userGroupId,
81
+ requestOptions,
82
+ );
83
+ data = response.data;
84
+ break;
85
+ }
86
+ case PermissionScope.ENVIRONMENT: {
87
+ const response =
88
+ await environmentPermissionsApi.getEnvironmentPermission(
89
+ organizationName,
90
+ resolved.environmentName as string,
91
+ userGroupId,
92
+ requestOptions,
93
+ );
94
+ data = response.data;
95
+ break;
96
+ }
97
+ default: {
98
+ const response =
99
+ await organizationPermissionsApi.getOrganizationPermission(
100
+ organizationName,
101
+ userGroupId,
102
+ requestOptions,
103
+ );
104
+ data = response.data;
105
+ break;
106
+ }
107
+ }
108
+
109
+ console.log(JSON.stringify(data, null, 2));
110
+ } catch (error) {
111
+ logger.error("Failed to get permission");
112
+ printOutError(error, debug);
113
+ process.exit(1);
114
+ }
115
+ }
@@ -0,0 +1,101 @@
1
+ import {
2
+ EnvironmentPermissionsApi,
3
+ OrganizationPermissionsApi,
4
+ PackagePermissionsApi,
5
+ } from "../../client/api";
6
+ import { Configuration } from "../../client/configuration";
7
+ import { ClientConfiguration } from "../../clientconfiguration";
8
+ import { logger } from "../../logger";
9
+ import { printOutError } from "../../utils";
10
+ import {
11
+ describeScope,
12
+ PermissionScope,
13
+ resolveScope,
14
+ ScopeOptions,
15
+ } from "./scope";
16
+
17
+ const organizationPermissionsApi = new OrganizationPermissionsApi(
18
+ new Configuration(),
19
+ );
20
+ const environmentPermissionsApi = new EnvironmentPermissionsApi(
21
+ new Configuration(),
22
+ );
23
+ const packagePermissionsApi = new PackagePermissionsApi(new Configuration());
24
+
25
+ export async function listPermissions(
26
+ controlPlane: string,
27
+ debug: boolean,
28
+ options: ScopeOptions,
29
+ ) {
30
+ const clientConfig = new ClientConfiguration();
31
+
32
+ if (!clientConfig.doesConfigExists()) {
33
+ logger.error("Please, make sure that you are logged in");
34
+ return;
35
+ }
36
+
37
+ const organizationName = clientConfig.getOrganization();
38
+ if (!organizationName) {
39
+ logger.error("No organization set. Please login or set an organization.");
40
+ return;
41
+ }
42
+
43
+ const resolved = resolveScope(options, clientConfig.getProject());
44
+ if ("error" in resolved) {
45
+ logger.error(resolved.error);
46
+ process.exit(1);
47
+ return;
48
+ }
49
+
50
+ logger.info(
51
+ `Listing permissions on ${describeScope(organizationName, resolved)}`,
52
+ );
53
+
54
+ const requestOptions = {
55
+ baseURL: controlPlane,
56
+ headers: {
57
+ Authorization: clientConfig.getAuthenticationHeader(),
58
+ },
59
+ };
60
+
61
+ try {
62
+ let data: unknown;
63
+ switch (resolved.scope) {
64
+ case PermissionScope.PACKAGE: {
65
+ const response = await packagePermissionsApi.listPackagePermissions(
66
+ organizationName,
67
+ resolved.environmentName as string,
68
+ resolved.packageName as string,
69
+ requestOptions,
70
+ );
71
+ data = response.data;
72
+ break;
73
+ }
74
+ case PermissionScope.ENVIRONMENT: {
75
+ const response =
76
+ await environmentPermissionsApi.listEnvironmentPermissions(
77
+ organizationName,
78
+ resolved.environmentName as string,
79
+ requestOptions,
80
+ );
81
+ data = response.data;
82
+ break;
83
+ }
84
+ default: {
85
+ const response =
86
+ await organizationPermissionsApi.listOrganizationPermissions(
87
+ organizationName,
88
+ requestOptions,
89
+ );
90
+ data = response.data;
91
+ break;
92
+ }
93
+ }
94
+
95
+ console.log(JSON.stringify(data, null, 2));
96
+ } catch (error) {
97
+ logger.error("Failed to list permissions");
98
+ printOutError(error, debug);
99
+ process.exit(1);
100
+ }
101
+ }
@@ -0,0 +1,180 @@
1
+ import {
2
+ EnvironmentPermissionPermissionEnum,
3
+ OrganizationPermissionPermissionEnum,
4
+ PackagePermissionPermissionEnum,
5
+ } from "../../client/api";
6
+ import { UserGroupId } from "../../common/UserGroupId";
7
+
8
+ /**
9
+ * Which resource a permission is granted on. The organization is the default
10
+ * because it is the only scope that names no resource of its own.
11
+ */
12
+ export enum PermissionScope {
13
+ ORGANIZATION = "organization",
14
+ ENVIRONMENT = "environment",
15
+ PACKAGE = "package",
16
+ }
17
+
18
+ /**
19
+ * The roles each scope accepts.
20
+ *
21
+ * The organization is the odd one out: it ends in `member` where the other two
22
+ * end in `viewer`. They are not interchangeable, so the sets are listed per
23
+ * scope rather than shared.
24
+ *
25
+ * A package takes `viewer` alone, which is narrower than its schema enum. The
26
+ * enum carries admin and modeler because a package reports them -- they are
27
+ * inherited from the environment -- but only viewer is directly assignable, and
28
+ * the server rejects the other two. Deriving this set from the enum would let
29
+ * the CLI accept a grant that cannot be written.
30
+ */
31
+ const ROLES_BY_SCOPE: Record<PermissionScope, readonly string[]> = {
32
+ [PermissionScope.ORGANIZATION]: Object.values(
33
+ OrganizationPermissionPermissionEnum,
34
+ ),
35
+ [PermissionScope.ENVIRONMENT]: Object.values(
36
+ EnvironmentPermissionPermissionEnum,
37
+ ),
38
+ [PermissionScope.PACKAGE]: [PackagePermissionPermissionEnum.Viewer],
39
+ };
40
+
41
+ export type ScopeOptions = {
42
+ environment?: string;
43
+ package?: string;
44
+ };
45
+
46
+ /**
47
+ * The resource a command addresses: the scope plus the names needed to reach
48
+ * it. `environmentName` is set for both the environment and package scopes,
49
+ * since a package is addressed through its environment.
50
+ */
51
+ export type ResolvedScope = {
52
+ scope: PermissionScope;
53
+ environmentName?: string;
54
+ packageName?: string;
55
+ };
56
+
57
+ export function rolesFor(scope: PermissionScope): readonly string[] {
58
+ return ROLES_BY_SCOPE[scope];
59
+ }
60
+
61
+ /**
62
+ * Works out which resource the options address, reading the environment from
63
+ * the config when a package is named without one.
64
+ *
65
+ * Returns an error rather than guessing when the environment cannot be
66
+ * resolved: silently falling back to the organization would grant a wider
67
+ * permission than the caller asked for.
68
+ */
69
+ export function resolveScope(
70
+ options: ScopeOptions,
71
+ configuredEnvironment: string | undefined,
72
+ ): ResolvedScope | { error: string } {
73
+ // An option that was passed but is empty -- `--environment "$UNSET_VAR"` --
74
+ // is a mistake, not an omission. Treating it as absent would widen the grant
75
+ // to the next scope out, which is the outcome this function exists to refuse,
76
+ // so it is reported rather than interpreted.
77
+ for (const name of ["environment", "package"] as const) {
78
+ const value = options[name];
79
+ if (value !== undefined && value.trim() === "") {
80
+ return { error: `--${name} was given an empty value.` };
81
+ }
82
+ }
83
+
84
+ if (options.package) {
85
+ const environmentName = options.environment || configuredEnvironment;
86
+ if (!environmentName) {
87
+ return {
88
+ error: "A package permission needs an environment. Pass --environment, or set one with 'cred set environment <name>'.",
89
+ };
90
+ }
91
+ return {
92
+ scope: PermissionScope.PACKAGE,
93
+ environmentName,
94
+ packageName: options.package,
95
+ };
96
+ }
97
+
98
+ if (options.environment) {
99
+ return {
100
+ scope: PermissionScope.ENVIRONMENT,
101
+ environmentName: options.environment,
102
+ };
103
+ }
104
+
105
+ return { scope: PermissionScope.ORGANIZATION };
106
+ }
107
+
108
+ /** Names the resource in log lines, so a message says what was addressed. */
109
+ export function describeScope(
110
+ organizationName: string,
111
+ resolved: ResolvedScope,
112
+ ): string {
113
+ switch (resolved.scope) {
114
+ case PermissionScope.PACKAGE:
115
+ return `package ${organizationName}/${resolved.environmentName}/${resolved.packageName}`;
116
+ case PermissionScope.ENVIRONMENT:
117
+ return `environment ${organizationName}/${resolved.environmentName}`;
118
+ default:
119
+ return `organization ${organizationName}`;
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Checks a role against the scope it is granted on, so a role the API would
125
+ * reject fails before the request instead of as an opaque 400.
126
+ */
127
+ export function validateRole(
128
+ scope: PermissionScope,
129
+ role: string,
130
+ ): string | null {
131
+ const allowed = rolesFor(scope);
132
+ if (!allowed.includes(role)) {
133
+ return `Invalid role '${role}' for a ${scope} permission. Expected one of: ${allowed.join(", ")}`;
134
+ }
135
+ return null;
136
+ }
137
+
138
+ /**
139
+ * The roles the authorization model accepts for public access, per scope.
140
+ *
141
+ * Narrower than the roles a named subject can hold, and not derivable from
142
+ * them: the model lists `user:*` on exactly three relations. Anything else
143
+ * reaches OpenFGA and fails there as a model validation error, so it is listed
144
+ * rather than special-cased.
145
+ */
146
+ const PUBLIC_ACCESS_ROLES_BY_SCOPE: Record<PermissionScope, readonly string[]> =
147
+ {
148
+ [PermissionScope.ORGANIZATION]: ["modeler"],
149
+ [PermissionScope.ENVIRONMENT]: ["viewer"],
150
+ [PermissionScope.PACKAGE]: ["viewer"],
151
+ };
152
+
153
+ /**
154
+ * Parses the subject and checks the roles public access may hold. Everything
155
+ * else about the subject -- whether a user's email is well formed, whether a
156
+ * group exists -- is left to the server, which is the authority on it.
157
+ */
158
+ export function parseSubject(
159
+ scope: PermissionScope,
160
+ role: string,
161
+ value: string,
162
+ ): { userGroupId: UserGroupId } | { error: string } {
163
+ let userGroupId: UserGroupId;
164
+ try {
165
+ userGroupId = new UserGroupId(value);
166
+ } catch (e) {
167
+ return { error: (e as Error).message };
168
+ }
169
+
170
+ if (userGroupId.isPublicAccess()) {
171
+ const allowed = PUBLIC_ACCESS_ROLES_BY_SCOPE[scope];
172
+ if (!allowed.includes(role)) {
173
+ return {
174
+ error: `Public access cannot be granted '${role}' on a ${scope}. Expected one of: ${allowed.join(", ")}`,
175
+ };
176
+ }
177
+ }
178
+
179
+ return { userGroupId };
180
+ }
@@ -7,6 +7,7 @@ import { deleteGroupAccessToken } from "./groupaccesstoken/deleteToken";
7
7
  import { deletePackage } from "./package/deletepackage";
8
8
  import { deleteEnvironment } from "./environment/deleteenvironment";
9
9
  import { warnProjectDeprecated } from "./legacy";
10
+ import { deletePermission } from "./permission/deletepermission";
10
11
 
11
12
  export const registerRmCommands = (cli: Command, environment: Environment) => {
12
13
  const rm = cli
@@ -14,6 +15,34 @@ export const registerRmCommands = (cli: Command, environment: Environment) => {
14
15
  .description("Remove a resource")
15
16
  .argument("<resource>", "The resource to remove");
16
17
 
18
+ rm.command("permission")
19
+ .description(
20
+ "Revoke a user or group's permission on the organization, an environment, or a package.",
21
+ )
22
+ .argument(
23
+ "<userGroupId>",
24
+ "Whose permission to revoke [user:{email}|group:{groupName}|user:*]",
25
+ )
26
+ .option(
27
+ "--environment <environmentName>",
28
+ "Revoke the permission on this environment instead of the organization.",
29
+ )
30
+ .option(
31
+ "--package <packageName>",
32
+ "Revoke the permission on this package. Uses --environment, or the current environment.",
33
+ )
34
+ .option("--debug")
35
+ .option("-y, --yes", "Skip confirmation.")
36
+ .action((userGroupId, options) => {
37
+ deletePermission(
38
+ controlPlanesByEnv[environment],
39
+ userGroupId,
40
+ options.debug || false,
41
+ options.yes === true,
42
+ { environment: options.environment, package: options.package },
43
+ );
44
+ });
45
+
17
46
  rm.command("package")
18
47
  .description("Delete a package.")
19
48
  .argument("<packageName>", "The name of the package to delete.")
@@ -20,40 +20,63 @@ export const registerUpdateCommands = (
20
20
  environmentName,
21
21
  {
22
22
  replicationCount: options.replication,
23
+ gitModeling: options.gitModeling,
24
+ gitRepositoryOwner: options.gitRepositoryOwner,
25
+ gitRepositoryName: options.gitRepositoryName,
26
+ gitBaseBranch: options.gitBaseBranch,
23
27
  },
24
28
  options.debug,
25
29
  options.yes,
26
30
  );
27
31
  };
28
32
 
29
- update
30
- .command("environment")
31
- .description("Update an environment.")
32
- .argument("<environmentName>", "The name of the environment to update.")
33
- .option(
34
- "--replication <replicationCount>",
35
- "The new replication count (must be at least 1).",
36
- parseInt,
37
- )
38
- .option("--debug")
39
- .option("-y, --yes", "Skip confirmation.")
40
- .action(updateEnvironmentAction);
33
+ // Declared once and applied to both the environment command and its deprecated
34
+ // project alias, so the alias cannot drift into accepting a different set.
35
+ const environmentOptions = (command: Command) =>
36
+ command
37
+ .option(
38
+ "--replication <replicationCount>",
39
+ "The new replication count (must be at least 1).",
40
+ parseInt,
41
+ )
42
+ .option(
43
+ "--git-modeling <true|false>",
44
+ "Enroll (or un-enroll) this environment for git-backed modeling drafts.",
45
+ )
46
+ .option(
47
+ "--git-repository-owner <owner>",
48
+ "GitHub owner of the repository holding this environment's draft branches.",
49
+ )
50
+ .option(
51
+ "--git-repository-name <name>",
52
+ "GitHub repository name, paired with --git-repository-owner.",
53
+ )
54
+ .option(
55
+ "--git-base-branch <branch>",
56
+ 'Branch draft branches fork from. Pass "" to all three git-repository options to unset the repository.',
57
+ )
58
+ .option("--debug")
59
+ .option("-y, --yes", "Skip confirmation.");
41
60
 
42
- update
43
- .command("project")
44
- .description("[deprecated] Alias for 'update environment'.")
45
- .argument("<projectName>", "The name of the environment to update.")
46
- .option(
47
- "--replication <replicationCount>",
48
- "The new replication count (must be at least 1).",
49
- parseInt,
50
- )
51
- .option("--debug")
52
- .option("-y, --yes", "Skip confirmation.")
53
- .action((projectName, options) => {
54
- warnProjectDeprecated("update environment");
55
- updateEnvironmentAction(projectName, options);
56
- });
61
+ environmentOptions(
62
+ update
63
+ .command("environment")
64
+ .description("Update an environment.")
65
+ .argument(
66
+ "<environmentName>",
67
+ "The name of the environment to update.",
68
+ ),
69
+ ).action(updateEnvironmentAction);
70
+
71
+ environmentOptions(
72
+ update
73
+ .command("project")
74
+ .description("[deprecated] Alias for 'update environment'.")
75
+ .argument("<projectName>", "The name of the environment to update."),
76
+ ).action((projectName, options) => {
77
+ warnProjectDeprecated("update environment");
78
+ updateEnvironmentAction(projectName, options);
79
+ });
57
80
 
58
81
  update
59
82
  .command("package")
@@ -107,11 +107,9 @@ describe("UserGroupId", () => {
107
107
  expect(id.isPublicAccess()).to.be.false;
108
108
  });
109
109
 
110
- it("should return false for user:* (checks originalValue not resourceName)", () => {
111
- // isPublicAccess checks this.originalValue === "*" which is never true
112
- // since originalValue is the full string "user:*"
110
+ it("should return true for user:* (the public-access wildcard)", () => {
113
111
  const id = UserGroupId.forPublicAccess();
114
- expect(id.isPublicAccess()).to.be.false;
112
+ expect(id.isPublicAccess()).to.be.true;
115
113
  });
116
114
  });
117
115
  });
@@ -131,7 +131,7 @@ export class UserGroupId {
131
131
 
132
132
  public isPublicAccess(): boolean {
133
133
  return (
134
- this.resourceType === ResourceType.USER && this.originalValue === "*"
134
+ this.resourceType === ResourceType.USER && this.resourceName === "*"
135
135
  );
136
136
  }
137
137
  }