|
| 1 | +package aipcli |
| 2 | + |
| 3 | +import ( |
| 4 | + "os" |
| 5 | + "strings" |
| 6 | + |
| 7 | + "github.com/spf13/cobra" |
| 8 | + "google.golang.org/genproto/googleapis/iam/v1" |
| 9 | + "google.golang.org/protobuf/encoding/protojson" |
| 10 | +) |
| 11 | + |
| 12 | +// NewIAMCommand returns a *cobra.Command for the google.iam.v1.IAMPolicy service. |
| 13 | +func NewIAMCommand() *cobra.Command { |
| 14 | + cmd := &cobra.Command{ |
| 15 | + Use: "iam", |
| 16 | + Short: "IAM policy service", |
| 17 | + Long: strings.TrimSpace(` |
| 18 | +Manages Identity and Access Management (IAM) policies. |
| 19 | +
|
| 20 | +Any implementation of an API that offers access control features |
| 21 | +implements the google.iam.v1.IAMPolicy interface. |
| 22 | +
|
| 23 | +Access control is applied when a principal (user or service account), takes |
| 24 | +some action on a resource exposed by a service. Resources, identified by |
| 25 | +URI-like names, are the unit of access control specification. Service |
| 26 | +implementations can choose the granularity of access control and the |
| 27 | +supported permissions for their resources. |
| 28 | +
|
| 29 | +For example one database service may allow access control to be |
| 30 | +specified only at the Table level, whereas another might allow access control |
| 31 | +to also be specified at the Column level. |
| 32 | +
|
| 33 | +This is intentionally not a CRUD style API because access control policies |
| 34 | +are created and deleted implicitly with the resources to which they are |
| 35 | +attached. |
| 36 | + `), |
| 37 | + } |
| 38 | + cmd.AddCommand(newSetIAMPolicyCommand()) |
| 39 | + cmd.AddCommand(newGetIAMPolicyCommand()) |
| 40 | + cmd.AddCommand(newAddIAMPolicyBindingCommand()) |
| 41 | + cmd.AddCommand(newRemoveIAMPolicyBindingCommand()) |
| 42 | + return cmd |
| 43 | +} |
| 44 | + |
| 45 | +func newGetIAMPolicyCommand() *cobra.Command { |
| 46 | + cmd := &cobra.Command{ |
| 47 | + Use: "get-iam-policy", |
| 48 | + Short: "get the IAM policy for a resource", |
| 49 | + Long: strings.TrimSpace(` |
| 50 | +Get the IAM policy associated with a resource. |
| 51 | +
|
| 52 | +The output includes an "etag" identifier that is used to check for |
| 53 | +concurrent policy updates. An edited policy should include the same |
| 54 | +etag so that set-iam-policy applies the changes to the correct policy |
| 55 | +version. |
| 56 | +
|
| 57 | +This command can fail for the following reasons: |
| 58 | +
|
| 59 | + ▪ The resource specified does not exist. |
| 60 | + ▪ The active account does not have permission to access the given |
| 61 | + resource's IAM policies. |
| 62 | +`), |
| 63 | + } |
| 64 | + resource := cmd.Flags().String("resource", "", "the resource for which the policy is being requested") |
| 65 | + _ = cmd.MarkFlagRequired("resource") |
| 66 | + _ = cmd.RegisterFlagCompletionFunc("resource", completeResource) |
| 67 | + cmd.RunE = func(cmd *cobra.Command, args []string) error { |
| 68 | + return invoke( |
| 69 | + cmd.Context(), |
| 70 | + "/google.iam.v1.IAMPolicy/GetIamPolicy", |
| 71 | + &iam.GetIamPolicyRequest{Resource: *resource}, |
| 72 | + &iam.Policy{}, |
| 73 | + ) |
| 74 | + } |
| 75 | + return cmd |
| 76 | +} |
| 77 | + |
| 78 | +func newSetIAMPolicyCommand() *cobra.Command { |
| 79 | + cmd := &cobra.Command{ |
| 80 | + Use: "set-iam-policy", |
| 81 | + Short: "set the IAM policy for a resource", |
| 82 | + Long: strings.TrimSpace(` |
| 83 | +Set the IAM policy associated with a resource. |
| 84 | +
|
| 85 | +This command can fail for the following reasons: |
| 86 | +
|
| 87 | + ▪ The resource specified does not exist. |
| 88 | + ▪ The active account does not have permission to access the given |
| 89 | + resource's IAM policies. |
| 90 | +`), |
| 91 | + } |
| 92 | + resource := cmd.Flags().String("resource", "", "the resource for which the policy is being specified") |
| 93 | + _ = cmd.MarkFlagRequired("resource") |
| 94 | + _ = cmd.RegisterFlagCompletionFunc("resource", completeResource) |
| 95 | + policyFile := cmd.Flags().String("policy-file", "", "path to a local JSON file containing a valid policy") |
| 96 | + _ = cmd.MarkFlagRequired("policy-file") |
| 97 | + _ = cmd.MarkFlagFilename("policy-file", "json") |
| 98 | + cmd.RunE = func(cmd *cobra.Command, args []string) error { |
| 99 | + data, err := os.ReadFile(*policyFile) |
| 100 | + if err != nil { |
| 101 | + return err |
| 102 | + } |
| 103 | + var policy iam.Policy |
| 104 | + if err := protojson.Unmarshal(data, &policy); err != nil { |
| 105 | + return err |
| 106 | + } |
| 107 | + return invoke( |
| 108 | + cmd.Context(), |
| 109 | + "/google.iam.v1.IAMPolicy/SetIamPolicy", |
| 110 | + &iam.SetIamPolicyRequest{Resource: *resource, Policy: &policy}, |
| 111 | + &iam.Policy{}, |
| 112 | + ) |
| 113 | + } |
| 114 | + return cmd |
| 115 | +} |
| 116 | + |
| 117 | +func newAddIAMPolicyBindingCommand() *cobra.Command { |
| 118 | + cmd := &cobra.Command{ |
| 119 | + Use: "add-iam-policy-binding", |
| 120 | + Short: "add an IAM policy binding to the IAM policy of a resource", |
| 121 | + Long: strings.TrimSpace(` |
| 122 | +Adds an IAM policy binding to the IAM policy of a resource. |
| 123 | +
|
| 124 | +One binding consists of a member, a role, and an optional condition. |
| 125 | +
|
| 126 | +This command can fail for the following reasons: |
| 127 | +
|
| 128 | + ▪ The resource specified does not exist. |
| 129 | + ▪ The active account does not have permission to access the given |
| 130 | + resource's IAM policies. |
| 131 | +`), |
| 132 | + } |
| 133 | + resource := cmd.Flags().String("resource", "", "the resource for which the policy binding is being added") |
| 134 | + _ = cmd.MarkFlagRequired("resource") |
| 135 | + _ = cmd.RegisterFlagCompletionFunc("resource", completeResource) |
| 136 | + member := cmd.Flags().String("member", "", "principal to add the binding for") |
| 137 | + _ = cmd.MarkFlagRequired("member") |
| 138 | + _ = cmd.RegisterFlagCompletionFunc("member", completeMember) |
| 139 | + role := cmd.Flags().String("role", "", "role name to assign to the principal") |
| 140 | + _ = cmd.MarkFlagRequired("role") |
| 141 | + _ = cmd.RegisterFlagCompletionFunc("role", completeRole) |
| 142 | + cmd.RunE = func(cmd *cobra.Command, args []string) error { |
| 143 | + var policy iam.Policy |
| 144 | + if err := invoke( |
| 145 | + cmd.Context(), |
| 146 | + "/google.iam.v1.IAMPolicy/GetIamPolicy", |
| 147 | + &iam.GetIamPolicyRequest{Resource: *resource}, |
| 148 | + &policy, |
| 149 | + ); err != nil { |
| 150 | + return err |
| 151 | + } |
| 152 | + addBinding(&policy, *member, *role) |
| 153 | + return invoke( |
| 154 | + cmd.Context(), |
| 155 | + "/google.iam.v1.IAMPolicy/SetIamPolicy", |
| 156 | + &iam.SetIamPolicyRequest{Resource: *resource, Policy: &policy}, |
| 157 | + &iam.Policy{}, |
| 158 | + ) |
| 159 | + } |
| 160 | + return cmd |
| 161 | +} |
| 162 | + |
| 163 | +func newRemoveIAMPolicyBindingCommand() *cobra.Command { |
| 164 | + cmd := &cobra.Command{ |
| 165 | + Use: "remove-iam-policy-binding", |
| 166 | + Short: "remove an IAM policy binding from the IAM policy of a resource", |
| 167 | + Long: strings.TrimSpace(` |
| 168 | +Removes an IAM policy binding from the IAM policy of a resource. |
| 169 | +One binding consists of a member, a role, and an optional condition. |
| 170 | +
|
| 171 | +This command can fail for the following reasons: |
| 172 | +
|
| 173 | + ▪ The resource specified does not exist. |
| 174 | + ▪ The active account does not have permission to access the given |
| 175 | + resource's IAM policies. |
| 176 | +`), |
| 177 | + } |
| 178 | + resource := cmd.Flags().String("resource", "", "the resource for which the policy binding is being removed") |
| 179 | + _ = cmd.MarkFlagRequired("resource") |
| 180 | + _ = cmd.RegisterFlagCompletionFunc("resource", completeResource) |
| 181 | + member := cmd.Flags().String("member", "", "principal to remove the binding for") |
| 182 | + _ = cmd.MarkFlagRequired("member") |
| 183 | + _ = cmd.RegisterFlagCompletionFunc("member", completeMember) |
| 184 | + role := cmd.Flags().String("role", "", "role name to remove the principal from") |
| 185 | + _ = cmd.MarkFlagRequired("role") |
| 186 | + _ = cmd.RegisterFlagCompletionFunc("role", completeRole) |
| 187 | + cmd.RunE = func(cmd *cobra.Command, args []string) error { |
| 188 | + var policy iam.Policy |
| 189 | + if err := invoke( |
| 190 | + cmd.Context(), |
| 191 | + "/google.iam.v1.IAMPolicy/GetIamPolicy", |
| 192 | + &iam.GetIamPolicyRequest{Resource: *resource}, |
| 193 | + &policy, |
| 194 | + ); err != nil { |
| 195 | + return err |
| 196 | + } |
| 197 | + removeBinding(&policy, *member, *role) |
| 198 | + return invoke( |
| 199 | + cmd.Context(), |
| 200 | + "/google.iam.v1.IAMPolicy/SetIamPolicy", |
| 201 | + &iam.SetIamPolicyRequest{Resource: *resource, Policy: &policy}, |
| 202 | + &iam.Policy{}, |
| 203 | + ) |
| 204 | + } |
| 205 | + return cmd |
| 206 | +} |
| 207 | + |
| 208 | +func completeResource(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { |
| 209 | + return cobra.AppendActiveHelp( |
| 210 | + nil, |
| 211 | + "Please enter a valid resource name. Example: resources/1234", |
| 212 | + ), cobra.ShellCompDirectiveNoFileComp |
| 213 | +} |
| 214 | + |
| 215 | +func completeMember(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { |
| 216 | + return cobra.AppendActiveHelp( |
| 217 | + nil, |
| 218 | + "Please enter a valid member. Example: email:[email protected]", |
| 219 | + ), cobra.ShellCompDirectiveNoFileComp |
| 220 | +} |
| 221 | + |
| 222 | +func completeRole(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { |
| 223 | + // TODO: Use the QueryGrantableRoles method to auto-complete grantable roles. |
| 224 | + return cobra.AppendActiveHelp( |
| 225 | + nil, |
| 226 | + "Please enter a valid role name. Example: roles/example.admin", |
| 227 | + ), cobra.ShellCompDirectiveNoFileComp |
| 228 | +} |
| 229 | + |
| 230 | +func addBinding(policy *iam.Policy, member, role string) { |
| 231 | + // look for existing binding with this role and member |
| 232 | + for _, binding := range policy.Bindings { |
| 233 | + if binding.Role == role { |
| 234 | + for _, bindingMember := range binding.Members { |
| 235 | + if bindingMember == member { |
| 236 | + // already have a binding with this role and member |
| 237 | + return |
| 238 | + } |
| 239 | + } |
| 240 | + // already have a binding with this role, but not the member |
| 241 | + binding.Members = append(binding.Members, member) |
| 242 | + return |
| 243 | + } |
| 244 | + } |
| 245 | + // add a new binding with this role and member |
| 246 | + policy.Bindings = append(policy.Bindings, &iam.Binding{ |
| 247 | + Role: role, |
| 248 | + Members: []string{member}, |
| 249 | + }) |
| 250 | +} |
| 251 | + |
| 252 | +func removeBinding(policy *iam.Policy, member, role string) { |
| 253 | + for _, binding := range policy.Bindings { |
| 254 | + if binding.Role == role { |
| 255 | + binding.Members = removeMember(binding.Members, member) |
| 256 | + if len(binding.Members) == 0 { |
| 257 | + policy.Bindings = removeRole(policy.Bindings, role) |
| 258 | + } |
| 259 | + return |
| 260 | + } |
| 261 | + } |
| 262 | +} |
| 263 | + |
| 264 | +func removeMember(members []string, member string) []string { |
| 265 | + for i, candidate := range members { |
| 266 | + if candidate == member { |
| 267 | + return append(members[:i], members[i+1:]...) |
| 268 | + } |
| 269 | + } |
| 270 | + return members |
| 271 | +} |
| 272 | + |
| 273 | +func removeRole(bindings []*iam.Binding, role string) []*iam.Binding { |
| 274 | + for i, binding := range bindings { |
| 275 | + if binding.Role == role { |
| 276 | + return append(bindings[:i], bindings[i+1:]...) |
| 277 | + } |
| 278 | + } |
| 279 | + return bindings |
| 280 | +} |
0 commit comments