1
0
mirror of https://github.com/minio/mc.git synced 2025-04-18 10:04:03 +03:00
mc/cmd/idp-ldap-policy-subcommands.go

451 lines
13 KiB
Go

// Copyright (c) 2015-2023 MinIO, Inc.
//
// This file is part of MinIO Object Storage stack
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package cmd
import (
"errors"
"fmt"
"strings"
"time"
"github.com/charmbracelet/lipgloss"
"github.com/minio/cli"
json "github.com/minio/colorjson"
"github.com/minio/madmin-go/v3"
"github.com/minio/mc/pkg/probe"
"github.com/minio/minio-go/v7/pkg/set"
)
var idpLdapPolicyAttachFlags = []cli.Flag{
cli.StringFlag{
Name: "user, u",
Usage: "attach policy to user by DN or by login name",
},
cli.StringFlag{
Name: "group, g",
Usage: "attach policy to LDAP Group DN",
},
}
var idpLdapPolicyAttachCmd = cli.Command{
Name: "attach",
Usage: "attach a policy to an entity",
Action: mainIDPLdapPolicyAttach,
Before: setGlobalsFromContext,
Flags: append(idpLdapPolicyAttachFlags, globalFlags...),
OnUsageError: onUsageError,
CustomHelpTemplate: `NAME:
{{.HelpName}} - {{.Usage}}
USAGE:
{{.HelpName}} [FLAGS] TARGET POLICY [POLICY...] [ --user=USER | --group=GROUP ]
Exactly one "--user" or "--group" flag is required.
POLICY:
Name of a policy on the MinIO server.
FLAGS:
{{range .VisibleFlags}}{{.}}
{{end}}
EXAMPLES:
1. Attach policy "mypolicy" to a user
{{.Prompt}} {{.HelpName}} play/ mypolicy --user='uid=bobfisher,ou=people,ou=hwengg,dc=min,dc=io'
2. Attach policies "policy1" and "policy2" to a group
{{.Prompt}} {{.HelpName}} play/ policy1 policy2 --group='cn=projectb,ou=groups,ou=swengg,dc=min,dc=io'
`,
}
// Quote from AWS policy naming requirement (ref:
// https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_iam-quotas.html):
//
// Names of users, groups, roles, policies, instance profiles, and server
// certificates must be alphanumeric, including the following common characters:
// plus (+), equal (=), comma (,), period (.), at (@), underscore (_), and
// hyphen (-).
func mainIDPLdapPolicyAttach(ctx *cli.Context) error {
// We need exactly one alias, and at least one policy.
if len(ctx.Args()) < 2 {
showCommandHelpAndExit(ctx, 1)
}
user := ctx.String("user")
group := ctx.String("group")
args := ctx.Args()
aliasedURL := args.Get(0)
policies := args[1:]
req := madmin.PolicyAssociationReq{
Policies: policies,
User: user,
Group: group,
}
fatalIf(probe.NewError(req.IsValid()), "Invalid policy attach arguments.")
// Create a new MinIO Admin Client
client, err := newAdminClient(aliasedURL)
fatalIf(err, "Unable to initialize admin connection.")
res, e := client.AttachPolicyLDAP(globalContext, req)
fatalIf(probe.NewError(e), "Unable to make LDAP policy association")
m := policyAssociationMessage{
attach: true,
Status: "success",
PoliciesAttached: res.PoliciesAttached,
User: user,
Group: group,
}
printMsg(m)
return nil
}
type policyAssociationMessage struct {
attach bool
Status string `json:"status"`
PoliciesAttached []string `json:"policiesAttached,omitempty"`
PoliciesDetached []string `json:"policiesDetached,omitempty"`
User string `json:"user,omitempty"`
Group string `json:"group,omitempty"`
}
func (m policyAssociationMessage) String() string {
style := lipgloss.NewStyle().Foreground(lipgloss.Color("#04B575")) // green
policiesS := style.Render("Attached Policies:")
entityS := style.Render("To User:")
policies := m.PoliciesAttached
entity := m.User
switch {
case m.User != "" && m.attach:
case m.User != "" && !m.attach:
policiesS = style.Render("Detached Policies:")
policies = m.PoliciesDetached
entityS = style.Render("From User:")
case m.Group != "" && m.attach:
entityS = style.Render("To Group:")
entity = m.Group
case m.Group != "" && !m.attach:
policiesS = style.Render("Detached Policies:")
policies = m.PoliciesDetached
entityS = style.Render("From Group:")
entity = m.Group
}
return fmt.Sprintf("%s %v\n%s %s\n", policiesS, policies, entityS, entity)
}
func (m policyAssociationMessage) JSON() string {
jsonMessageBytes, e := json.MarshalIndent(m, "", " ")
fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
return string(jsonMessageBytes)
}
var idpLdapPolicyDetachFlags = []cli.Flag{
cli.StringFlag{
Name: "user, u",
Usage: "attach policy to user by DN or by login name",
},
cli.StringFlag{
Name: "group, g",
Usage: "attach policy to LDAP Group DN",
},
}
var idpLdapPolicyDetachCmd = cli.Command{
Name: "detach",
Usage: "detach a policy from an entity",
Action: mainIDPLdapPolicyDetach,
Before: setGlobalsFromContext,
Flags: append(idpLdapPolicyDetachFlags, globalFlags...),
OnUsageError: onUsageError,
CustomHelpTemplate: `NAME:
{{.HelpName}} - {{.Usage}}
USAGE:
{{.HelpName}} [FLAGS] TARGET POLICY [POLICY...] [ --user=USER | --group=GROUP ]
Exactly one of "--user" or "--group" is required.
POLICY:
Name of a policy on the MinIO server.
FLAGS:
{{range .VisibleFlags}}{{.}}
{{end}}
EXAMPLES:
1. Detach policy "mypolicy" from a user
{{.Prompt}} {{.HelpName}} play/ mypolicy --user='uid=bobfisher,ou=people,ou=hwengg,dc=min,dc=io'
2. Detach policies "policy1" and "policy2" from a group
{{.Prompt}} {{.HelpName}} play/ policy1 policy2 --group='cn=projectb,ou=groups,ou=swengg,dc=min,dc=io'
`,
}
// Quote from AWS policy naming requirement (ref:
// https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_iam-quotas.html):
//
// Names of users, groups, roles, policies, instance profiles, and server
// certificates must be alphanumeric, including the following common characters:
// plus (+), equal (=), comma (,), period (.), at (@), underscore (_), and
// hyphen (-).
func mainIDPLdapPolicyDetach(ctx *cli.Context) error {
// We need exactly one alias, and at least one policy.
if len(ctx.Args()) < 2 {
showCommandHelpAndExit(ctx, 1)
}
user := ctx.String("user")
group := ctx.String("group")
if user == "" && group == "" {
e := errors.New("at least one of --user or --group is required.")
fatalIf(probe.NewError(e), "Missing flag in command")
}
args := ctx.Args()
aliasedURL := args.Get(0)
policies := args[1:]
// Create a new MinIO Admin Client
client, err := newAdminClient(aliasedURL)
fatalIf(err, "Unable to initialize admin connection.")
res, e := client.DetachPolicyLDAP(globalContext,
madmin.PolicyAssociationReq{
Policies: policies,
User: user,
Group: group,
})
fatalIf(probe.NewError(e), "Unable to make LDAP policy association")
m := policyAssociationMessage{
attach: false,
Status: "success",
PoliciesDetached: res.PoliciesDetached,
User: user,
Group: group,
}
printMsg(m)
return nil
}
var idpLdapPolicyEntitiesFlags = []cli.Flag{
cli.StringSliceFlag{
Name: "user, u",
Usage: "list policies associated with user(s)",
},
cli.StringSliceFlag{
Name: "group, g",
Usage: "list policies associated with group(s)",
},
cli.StringSliceFlag{
Name: "policy, p",
Usage: "list users or groups associated with policy",
},
}
var idpLdapPolicyEntitiesCmd = cli.Command{
Name: "entities",
Usage: "list policy association entities",
Action: mainIDPLdapPolicyEntities,
Before: setGlobalsFromContext,
Flags: append(idpLdapPolicyEntitiesFlags, globalFlags...),
OnUsageError: onUsageError,
CustomHelpTemplate: `NAME:
{{.HelpName}} - {{.Usage}}
USAGE:
{{.HelpName}} [FLAGS] TARGET
FLAGS:
{{range .VisibleFlags}}{{.}}
{{end}}
EXAMPLES:
1. List all LDAP entities associated with all policies
{{.Prompt}} {{.HelpName}} play/
2. List all LDAP entities associated with the policies 'finteam-policy' and 'mlteam-policy'
{{.Prompt}} {{.HelpName}} play/ --policy finteam-policy --policy mlteam-policy
3. List all policies associated with a pair of User LDAP entities
{{.Prompt}} {{.HelpName}} play/ \
--user 'uid=bobfisher,ou=people,ou=hwengg,dc=min,dc=io' \
--user 'uid=fahim,ou=people,ou=swengg,dc=min,dc=io'
4. List all policies associated with a pair of Group LDAP entities
{{.Prompt}} {{.HelpName}} play/ \
--group 'cn=projecta,ou=groups,ou=swengg,dc=min,dc=io' \
--group 'cn=projectb,ou=groups,ou=swengg,dc=min,dc=io'
5. List all entities associated with a policy, group and user
{{.Prompt}} {{.HelpName}} play/ \
--policy finteam-policy
--user 'uid=bobfisher,ou=people,ou=hwengg,dc=min,dc=io' \
--group 'cn=projectb,ou=groups,ou=swengg,dc=min,dc=io'
`,
}
func mainIDPLdapPolicyEntities(ctx *cli.Context) error {
if len(ctx.Args()) != 1 {
showCommandHelpAndExit(ctx, 1)
}
usersToQuery := ctx.StringSlice("user")
groupsToQuery := ctx.StringSlice("group")
policiesToQuery := ctx.StringSlice("policy")
args := ctx.Args()
aliasedURL := args.Get(0)
// Create a new MinIO Admin Client
client, err := newAdminClient(aliasedURL)
fatalIf(err, "Unable to initialize admin connection.")
res, e := client.GetLDAPPolicyEntities(globalContext,
madmin.PolicyEntitiesQuery{
Users: usersToQuery,
Groups: groupsToQuery,
Policy: policiesToQuery,
})
fatalIf(probe.NewError(e), "Unable to fetch LDAP policy entities")
printMsg(policyEntitiesFrom(res))
return nil
}
type policyEntities struct {
Status string `json:"status"`
Result madmin.PolicyEntitiesResult `json:"result"`
}
func policyEntitiesFrom(r madmin.PolicyEntitiesResult) policyEntities {
return policyEntities{
Status: "success",
Result: r,
}
}
func (p policyEntities) JSON() string {
bs, e := json.MarshalIndent(p, "", " ")
fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
return string(bs)
}
func iFmt(n int, fmtStr string, a ...any) string {
indentStr := ""
if n > 0 {
s := make([]rune, n)
for i := range s {
s[i] = ' '
}
indentStr = string(s)
}
return fmt.Sprintf(indentStr+fmtStr, a...)
}
func builderWrapper(strList []string, o *strings.Builder, indent, maxLen int) {
currLen := 0
for _, s := range strList {
if currLen+len(s) > maxLen && currLen > 0 {
o.WriteString("\n")
currLen = 0
}
if currLen == 0 {
o.WriteString(iFmt(indent, ""))
currLen = indent
} else {
o.WriteString(", ")
currLen += 2
}
if strings.Contains(s, ",") {
s = fmt.Sprintf("\"%s\"", s)
}
o.WriteString(s)
currLen += len(s)
}
o.WriteString("\n")
}
func (p policyEntities) String() string {
labelStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#04B575")) // green
o := strings.Builder{}
o.WriteString(iFmt(0, "%s %s\n",
labelStyle.Render("Query time:"),
p.Result.Timestamp.Format(time.RFC3339)))
if len(p.Result.UserMappings) > 0 {
o.WriteString(iFmt(0, "%s\n", labelStyle.Render("User -> Policy Mappings:")))
for _, u := range p.Result.UserMappings {
o.WriteString(iFmt(2, "%s %s\n", labelStyle.Render("User:"), u.User))
o.WriteString(iFmt(4, "%s\n", labelStyle.Render("Policies:")))
builderWrapper(u.Policies, &o, 6, 80)
if len(u.MemberOfMappings) > 0 {
effectivePolicies := set.CreateStringSet(u.Policies...)
o.WriteString(iFmt(4, "%s\n", labelStyle.Render("Group Memberships:")))
groups := make([]string, 0, len(u.MemberOfMappings))
for _, g := range u.MemberOfMappings {
groups = append(groups, g.Group)
for _, p := range g.Policies {
effectivePolicies.Add(p)
}
}
builderWrapper(groups, &o, 6, 80)
o.WriteString(iFmt(4, "%s\n", labelStyle.Render("Effective Policies:")))
builderWrapper(effectivePolicies.ToSlice(), &o, 6, 80)
}
}
}
if len(p.Result.GroupMappings) > 0 {
o.WriteString(iFmt(0, "%s\n", labelStyle.Render("Group -> Policy Mappings:")))
for _, u := range p.Result.GroupMappings {
o.WriteString(iFmt(2, "%s %s\n", labelStyle.Render("Group:"), u.Group))
o.WriteString(iFmt(4, "%s\n", labelStyle.Render("Policies:")))
for _, p := range u.Policies {
o.WriteString(iFmt(6, "%s\n", p))
}
}
}
if len(p.Result.PolicyMappings) > 0 {
o.WriteString(iFmt(0, "%s\n", labelStyle.Render("Policy -> Entity Mappings:")))
for _, u := range p.Result.PolicyMappings {
o.WriteString(iFmt(2, "%s %s\n", labelStyle.Render("Policy:"), u.Policy))
if len(u.Users) > 0 {
o.WriteString(iFmt(4, "%s\n", labelStyle.Render("User Mappings:")))
for _, p := range u.Users {
o.WriteString(iFmt(6, "%s\n", p))
}
}
if len(u.Groups) > 0 {
o.WriteString(iFmt(4, "%s\n", labelStyle.Render("Group Mappings:")))
for _, p := range u.Groups {
o.WriteString(iFmt(6, "%s\n", p))
}
}
}
}
return o.String()
}