github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/idp-ldap-policy-subcommands.go (about) 1 // Copyright (c) 2015-2023 MinIO, Inc. 2 // 3 // This file is part of MinIO Object Storage stack 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package cmd 19 20 import ( 21 "errors" 22 "fmt" 23 "strings" 24 "time" 25 26 "github.com/charmbracelet/lipgloss" 27 "github.com/minio/cli" 28 json "github.com/minio/colorjson" 29 "github.com/minio/madmin-go/v3" 30 "github.com/minio/mc/pkg/probe" 31 ) 32 33 var idpLdapPolicyAttachFlags = []cli.Flag{ 34 cli.StringFlag{ 35 Name: "user, u", 36 Usage: "attach policy to user by DN or by login name", 37 }, 38 cli.StringFlag{ 39 Name: "group, g", 40 Usage: "attach policy to LDAP Group DN", 41 }, 42 } 43 44 var idpLdapPolicyAttachCmd = cli.Command{ 45 Name: "attach", 46 Usage: "attach a policy to an entity", 47 Action: mainIDPLdapPolicyAttach, 48 Before: setGlobalsFromContext, 49 Flags: append(idpLdapPolicyAttachFlags, globalFlags...), 50 OnUsageError: onUsageError, 51 CustomHelpTemplate: `NAME: 52 {{.HelpName}} - {{.Usage}} 53 54 USAGE: 55 {{.HelpName}} [FLAGS] TARGET POLICY [POLICY...] [ --user=USER | --group=GROUP ] 56 57 Exactly one "--user" or "--group" flag is required. 58 59 POLICY: 60 Name of a policy on the MinIO server. 61 62 FLAGS: 63 {{range .VisibleFlags}}{{.}} 64 {{end}} 65 EXAMPLES: 66 1. Attach policy "mypolicy" to a user 67 {{.Prompt}} {{.HelpName}} play/ mypolicy --user='uid=bobfisher,ou=people,ou=hwengg,dc=min,dc=io' 68 2. Attach policies "policy1" and "policy2" to a group 69 {{.Prompt}} {{.HelpName}} play/ policy1 policy2 --group='cn=projectb,ou=groups,ou=swengg,dc=min,dc=io' 70 `, 71 } 72 73 // Quote from AWS policy naming requirement (ref: 74 // https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_iam-quotas.html): 75 // 76 // Names of users, groups, roles, policies, instance profiles, and server 77 // certificates must be alphanumeric, including the following common characters: 78 // plus (+), equal (=), comma (,), period (.), at (@), underscore (_), and 79 // hyphen (-). 80 81 func mainIDPLdapPolicyAttach(ctx *cli.Context) error { 82 // We need exactly one alias, and at least one policy. 83 if len(ctx.Args()) < 2 { 84 showCommandHelpAndExit(ctx, 1) 85 } 86 user := ctx.String("user") 87 group := ctx.String("group") 88 89 args := ctx.Args() 90 aliasedURL := args.Get(0) 91 92 policies := args[1:] 93 req := madmin.PolicyAssociationReq{ 94 Policies: policies, 95 User: user, 96 Group: group, 97 } 98 fatalIf(probe.NewError(req.IsValid()), "Invalid policy attach arguments.") 99 100 // Create a new MinIO Admin Client 101 client, err := newAdminClient(aliasedURL) 102 fatalIf(err, "Unable to initialize admin connection.") 103 104 res, e := client.AttachPolicyLDAP(globalContext, req) 105 fatalIf(probe.NewError(e), "Unable to make LDAP policy association") 106 107 m := policyAssociationMessage{ 108 attach: true, 109 Status: "success", 110 PoliciesAttached: res.PoliciesAttached, 111 User: user, 112 Group: group, 113 } 114 printMsg(m) 115 return nil 116 } 117 118 type policyAssociationMessage struct { 119 attach bool 120 Status string `json:"status"` 121 PoliciesAttached []string `json:"policiesAttached,omitempty"` 122 PoliciesDetached []string `json:"policiesDetached,omitempty"` 123 User string `json:"user,omitempty"` 124 Group string `json:"group,omitempty"` 125 } 126 127 func (m policyAssociationMessage) String() string { 128 style := lipgloss.NewStyle().Foreground(lipgloss.Color("#04B575")) // green 129 130 policiesS := style.Render("Attached Policies:") 131 entityS := style.Render("To User:") 132 policies := m.PoliciesAttached 133 entity := m.User 134 switch { 135 case m.User != "" && m.attach: 136 case m.User != "" && !m.attach: 137 policiesS = style.Render("Detached Policies:") 138 policies = m.PoliciesDetached 139 entityS = style.Render("From User:") 140 case m.Group != "" && m.attach: 141 entityS = style.Render("To Group:") 142 entity = m.Group 143 case m.Group != "" && !m.attach: 144 policiesS = style.Render("Detached Policies:") 145 policies = m.PoliciesDetached 146 entityS = style.Render("From Group:") 147 entity = m.Group 148 } 149 return fmt.Sprintf("%s %v\n%s %s\n", policiesS, policies, entityS, entity) 150 } 151 152 func (m policyAssociationMessage) JSON() string { 153 jsonMessageBytes, e := json.MarshalIndent(m, "", " ") 154 fatalIf(probe.NewError(e), "Unable to marshal into JSON.") 155 156 return string(jsonMessageBytes) 157 } 158 159 var idpLdapPolicyDetachFlags = []cli.Flag{ 160 cli.StringFlag{ 161 Name: "user, u", 162 Usage: "attach policy to user by DN or by login name", 163 }, 164 cli.StringFlag{ 165 Name: "group, g", 166 Usage: "attach policy to LDAP Group DN", 167 }, 168 } 169 170 var idpLdapPolicyDetachCmd = cli.Command{ 171 Name: "detach", 172 Usage: "detach a policy from an entity", 173 Action: mainIDPLdapPolicyDetach, 174 Before: setGlobalsFromContext, 175 Flags: append(idpLdapPolicyDetachFlags, globalFlags...), 176 OnUsageError: onUsageError, 177 CustomHelpTemplate: `NAME: 178 {{.HelpName}} - {{.Usage}} 179 180 USAGE: 181 {{.HelpName}} [FLAGS] TARGET POLICY [POLICY...] [ --user=USER | --group=GROUP ] 182 183 Exactly one of "--user" or "--group" is required. 184 185 POLICY: 186 Name of a policy on the MinIO server. 187 188 FLAGS: 189 {{range .VisibleFlags}}{{.}} 190 {{end}} 191 EXAMPLES: 192 1. Detach policy "mypolicy" from a user 193 {{.Prompt}} {{.HelpName}} play/ mypolicy --user='uid=bobfisher,ou=people,ou=hwengg,dc=min,dc=io' 194 2. Detach policies "policy1" and "policy2" from a group 195 {{.Prompt}} {{.HelpName}} play/ policy1 policy2 --group='cn=projectb,ou=groups,ou=swengg,dc=min,dc=io' 196 `, 197 } 198 199 // Quote from AWS policy naming requirement (ref: 200 // https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_iam-quotas.html): 201 // 202 // Names of users, groups, roles, policies, instance profiles, and server 203 // certificates must be alphanumeric, including the following common characters: 204 // plus (+), equal (=), comma (,), period (.), at (@), underscore (_), and 205 // hyphen (-). 206 207 func mainIDPLdapPolicyDetach(ctx *cli.Context) error { 208 // We need exactly one alias, and at least one policy. 209 if len(ctx.Args()) < 2 { 210 showCommandHelpAndExit(ctx, 1) 211 } 212 213 user := ctx.String("user") 214 group := ctx.String("group") 215 216 if user == "" && group == "" { 217 e := errors.New("at least one of --user or --group is required.") 218 fatalIf(probe.NewError(e), "Missing flag in command") 219 } 220 221 args := ctx.Args() 222 aliasedURL := args.Get(0) 223 224 policies := args[1:] 225 226 // Create a new MinIO Admin Client 227 client, err := newAdminClient(aliasedURL) 228 fatalIf(err, "Unable to initialize admin connection.") 229 230 res, e := client.DetachPolicyLDAP(globalContext, 231 madmin.PolicyAssociationReq{ 232 Policies: policies, 233 User: user, 234 Group: group, 235 }) 236 fatalIf(probe.NewError(e), "Unable to make LDAP policy association") 237 238 m := policyAssociationMessage{ 239 attach: false, 240 Status: "success", 241 PoliciesDetached: res.PoliciesDetached, 242 User: user, 243 Group: group, 244 } 245 printMsg(m) 246 return nil 247 } 248 249 var idpLdapPolicyEntitiesFlags = []cli.Flag{ 250 cli.StringSliceFlag{ 251 Name: "user, u", 252 Usage: "list policies associated with user(s)", 253 }, 254 cli.StringSliceFlag{ 255 Name: "group, g", 256 Usage: "list policies associated with group(s)", 257 }, 258 cli.StringSliceFlag{ 259 Name: "policy, p", 260 Usage: "list users or groups associated with policy", 261 }, 262 } 263 264 var idpLdapPolicyEntitiesCmd = cli.Command{ 265 Name: "entities", 266 Usage: "list policy association entities", 267 Action: mainIDPLdapPolicyEntities, 268 Before: setGlobalsFromContext, 269 Flags: append(idpLdapPolicyEntitiesFlags, globalFlags...), 270 OnUsageError: onUsageError, 271 CustomHelpTemplate: `NAME: 272 {{.HelpName}} - {{.Usage}} 273 274 USAGE: 275 {{.HelpName}} [FLAGS] TARGET 276 277 FLAGS: 278 {{range .VisibleFlags}}{{.}} 279 {{end}} 280 EXAMPLES: 281 1. List all LDAP entities associated with all policies 282 {{.Prompt}} {{.HelpName}} play/ 283 2. List all LDAP entities associated with the policies 'finteam-policy' and 'mlteam-policy' 284 {{.Prompt}} {{.HelpName}} play/ --policy finteam-policy --policy mlteam-policy 285 3. List all policies associated with a pair of User LDAP entities 286 {{.Prompt}} {{.HelpName}} play/ \ 287 --user 'uid=bobfisher,ou=people,ou=hwengg,dc=min,dc=io' \ 288 --user 'uid=fahim,ou=people,ou=swengg,dc=min,dc=io' 289 4. List all policies associated with a pair of Group LDAP entities 290 {{.Prompt}} {{.HelpName}} play/ \ 291 --group 'cn=projecta,ou=groups,ou=swengg,dc=min,dc=io' \ 292 --group 'cn=projectb,ou=groups,ou=swengg,dc=min,dc=io' 293 5. List all entities associated with a policy, group and user 294 {{.Prompt}} {{.HelpName}} play/ \ 295 --policy finteam-policy 296 --user 'uid=bobfisher,ou=people,ou=hwengg,dc=min,dc=io' \ 297 --group 'cn=projectb,ou=groups,ou=swengg,dc=min,dc=io' 298 `, 299 } 300 301 func mainIDPLdapPolicyEntities(ctx *cli.Context) error { 302 if len(ctx.Args()) != 1 { 303 showCommandHelpAndExit(ctx, 1) 304 } 305 306 usersToQuery := ctx.StringSlice("user") 307 groupsToQuery := ctx.StringSlice("group") 308 policiesToQuery := ctx.StringSlice("policy") 309 310 args := ctx.Args() 311 312 aliasedURL := args.Get(0) 313 314 // Create a new MinIO Admin Client 315 client, err := newAdminClient(aliasedURL) 316 fatalIf(err, "Unable to initialize admin connection.") 317 318 res, e := client.GetLDAPPolicyEntities(globalContext, 319 madmin.PolicyEntitiesQuery{ 320 Users: usersToQuery, 321 Groups: groupsToQuery, 322 Policy: policiesToQuery, 323 }) 324 fatalIf(probe.NewError(e), "Unable to fetch LDAP policy entities") 325 326 printMsg(policyEntitiesFrom(res)) 327 return nil 328 } 329 330 type policyEntities struct { 331 Status string `json:"status"` 332 Result madmin.PolicyEntitiesResult `json:"result"` 333 } 334 335 func policyEntitiesFrom(r madmin.PolicyEntitiesResult) policyEntities { 336 return policyEntities{ 337 Status: "success", 338 Result: r, 339 } 340 } 341 342 func (p policyEntities) JSON() string { 343 bs, e := json.MarshalIndent(p, "", " ") 344 fatalIf(probe.NewError(e), "Unable to marshal into JSON.") 345 346 return string(bs) 347 } 348 349 func iFmt(n int, fmtStr string, a ...any) string { 350 indentStr := "" 351 if n > 0 { 352 s := make([]rune, n) 353 for i := range s { 354 s[i] = ' ' 355 } 356 indentStr = string(s) 357 } 358 return fmt.Sprintf(indentStr+fmtStr, a...) 359 } 360 361 func (p policyEntities) String() string { 362 labelStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#04B575")) // green 363 o := strings.Builder{} 364 365 o.WriteString(iFmt(0, "%s %s\n", 366 labelStyle.Render("Query time:"), 367 p.Result.Timestamp.Format(time.RFC3339))) 368 369 if len(p.Result.UserMappings) > 0 { 370 o.WriteString(iFmt(0, "%s\n", labelStyle.Render("User -> Policy Mappings:"))) 371 372 for _, u := range p.Result.UserMappings { 373 o.WriteString(iFmt(2, "%s %s\n", labelStyle.Render("User:"), u.User)) 374 for _, p := range u.Policies { 375 o.WriteString(iFmt(4, "%s\n", p)) 376 } 377 } 378 } 379 if len(p.Result.GroupMappings) > 0 { 380 o.WriteString(iFmt(0, "%s\n", labelStyle.Render("Group -> Policy Mappings:"))) 381 382 for _, u := range p.Result.GroupMappings { 383 o.WriteString(iFmt(2, "%s %s\n", labelStyle.Render("Group:"), u.Group)) 384 for _, p := range u.Policies { 385 o.WriteString(iFmt(4, "%s\n", p)) 386 } 387 } 388 } 389 if len(p.Result.PolicyMappings) > 0 { 390 o.WriteString(iFmt(0, "%s\n", labelStyle.Render("Policy -> Entity Mappings:"))) 391 392 for _, u := range p.Result.PolicyMappings { 393 o.WriteString(iFmt(2, "%s %s\n", labelStyle.Render("Policy:"), u.Policy)) 394 if len(u.Users) > 0 { 395 o.WriteString(iFmt(4, "%s\n", labelStyle.Render("User Mappings:"))) 396 for _, p := range u.Users { 397 o.WriteString(iFmt(6, "%s\n", p)) 398 } 399 } 400 if len(u.Groups) > 0 { 401 o.WriteString(iFmt(4, "%s\n", labelStyle.Render("Group Mappings:"))) 402 for _, p := range u.Groups { 403 o.WriteString(iFmt(6, "%s\n", p)) 404 } 405 } 406 } 407 } 408 409 return o.String() 410 }