Files
object-browser/restapi/client.go
Lenin Alevski 438211199d LDAP authentication support for MCS (#114)
This PR adds ldap authentication support for mcs based on
https://github.com/minio/minio/blob/master/docs/sts/ldap.md

How to test:

```
$ docker run --rm -p 389:389 -p 636:636 --name my-openldap-container
--detach osixia/openldap:1.3.0
```

Run the `billy.ldif` file using `ldapadd` command to create a new user
and assign it to a group.

```
$ cat > billy.ldif << EOF
dn: uid=billy,dc=example,dc=org
uid: billy
cn: billy
sn: 3
objectClass: top
objectClass: posixAccount
objectClass: inetOrgPerson
loginShell: /bin/bash
homeDirectory: /home/billy
uidNumber: 14583102
gidNumber: 14564100
userPassword: {SSHA}j3lBh1Seqe4rqF1+NuWmjhvtAni1JC5A
mail: billy@example.org
gecos: Billy User
dn: ou=groups,dc=example,dc=org
objectclass:organizationalunit
ou: groups
description: generic groups branch
of s3::*)
dn: cn=mcsAdmin,ou=groups,dc=example,dc=org
objectClass: top
objectClass: posixGroup
gidNumber: 678
dn: cn=mcsAdmin,ou=groups,dc=example,dc=org
changetype: modify
add: memberuid
memberuid: billy
EOF

$ docker cp billy.ldif
my-openldap-container:/container/service/slapd/assets/test/billy.ldif
$ docker exec my-openldap-container ldapadd -x -D
"cn=admin,dc=example,dc=org" -w admin -f
/container/service/slapd/assets/test/billy.ldif -H ldap://localhost -ZZ
```

Query the ldap server to check the user billy was created correctly and
got assigned to the mcsAdmin group, you should get a list
containing ldap users and groups.

```
$ docker exec my-openldap-container ldapsearch -x -H ldap://localhost -b
dc=example,dc=org -D "cn=admin,dc=example,dc=org" -w admin
```

Query the ldap server again, this time filtering only for the user
`billy`, you should see only 1 record.

```
$ docker exec my-openldap-container ldapsearch -x -H ldap://localhost -b
uid=billy,dc=example,dc=org -D "cn=admin,dc=example,dc=org" -w admin
```

Change the password for user billy

Set the new password for `billy` to `minio123` and enter `admin` as the
default `LDAP Password`

```
$ docker exec -it my-openldap-container /bin/bash
ldappasswd -H ldap://localhost -x -D "cn=admin,dc=example,dc=org" -W
-S "uid=billy,dc=example,dc=org"
New password:
Re-enter new password:
Enter LDAP Password:
```

Add the mcsAdmin policy to user billy on MinIO

```
$ cat > mcsAdmin.json << EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": [
        "admin:*"
      ],
      "Effect": "Allow",
      "Sid": ""
    },
    {
      "Action": [
        "s3:*"
      ],
      "Effect": "Allow",
      "Resource": [
        "arn:aws:s3:::*"
      ],
      "Sid": ""
    }
  ]
}
EOF
$ mc admin policy add myminio mcsAdmin mcsAdmin.json
$ mc admin policy set myminio mcsAdmin user=billy
```

Run MinIO

```
export MINIO_ACCESS_KEY=minio
export MINIO_SECRET_KEY=minio123
export MINIO_IDENTITY_LDAP_SERVER_ADDR='localhost:389'
export MINIO_IDENTITY_LDAP_USERNAME_FORMAT='uid=%s,dc=example,dc=org'
export
MINIO_IDENTITY_LDAP_USERNAME_SEARCH_FILTER='(|(objectclass=posixAccount)(uid=%s))'
export MINIO_IDENTITY_LDAP_TLS_SKIP_VERIFY=on
export MINIO_IDENTITY_LDAP_SERVER_INSECURE=on
./minio server ~/Data
```

Run MCS

```
export MCS_ACCESS_KEY=minio
export MCS_SECRET_KEY=minio123
...
export MCS_LDAP_ENABLED=on
./mcs server
```
2020-05-12 10:26:38 -07:00

268 lines
8.9 KiB
Go

// This file is part of MinIO Orchestrator
// Copyright (c) 2020 MinIO, Inc.
//
// 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 restapi
import (
"context"
"fmt"
"errors"
mc "github.com/minio/mc/cmd"
"github.com/minio/mc/pkg/probe"
"github.com/minio/mcs/pkg/auth"
xjwt "github.com/minio/mcs/pkg/auth/jwt"
"github.com/minio/mcs/pkg/auth/ldap"
"github.com/minio/minio-go/v6"
"github.com/minio/minio-go/v6/pkg/credentials"
)
func init() {
// All minio-go API operations shall be performed only once,
// another way to look at this is we are turning off retries.
minio.MaxRetry = 1
}
// MinioClient interface with all functions to be implemented
// by mock when testing, it should include all MinioClient respective api calls
// that are used within this project.
type MinioClient interface {
listBucketsWithContext(ctx context.Context) ([]minio.BucketInfo, error)
makeBucketWithContext(ctx context.Context, bucketName, location string) error
setBucketPolicyWithContext(ctx context.Context, bucketName, policy string) error
removeBucket(bucketName string) error
getBucketNotification(bucketName string) (bucketNotification minio.BucketNotification, err error)
getBucketPolicy(bucketName string) (string, error)
}
// Interface implementation
//
// Define the structure of a minIO Client and define the functions that are actually used
// from minIO api.
type minioClient struct {
client *minio.Client
}
// implements minio.ListBucketsWithContext(ctx)
func (c minioClient) listBucketsWithContext(ctx context.Context) ([]minio.BucketInfo, error) {
return c.client.ListBucketsWithContext(ctx)
}
// implements minio.MakeBucketWithContext(ctx, bucketName, location)
func (c minioClient) makeBucketWithContext(ctx context.Context, bucketName, location string) error {
return c.client.MakeBucketWithContext(ctx, bucketName, location)
}
// implements minio.SetBucketPolicyWithContext(ctx, bucketName, policy)
func (c minioClient) setBucketPolicyWithContext(ctx context.Context, bucketName, policy string) error {
return c.client.SetBucketPolicyWithContext(ctx, bucketName, policy)
}
// implements minio.RemoveBucket(bucketName)
func (c minioClient) removeBucket(bucketName string) error {
return c.client.RemoveBucket(bucketName)
}
// implements minio.GetBucketNotification(bucketName)
func (c minioClient) getBucketNotification(bucketName string) (bucketNotification minio.BucketNotification, err error) {
return c.client.GetBucketNotification(bucketName)
}
// implements minio.GetBucketPolicy(bucketName)
func (c minioClient) getBucketPolicy(bucketName string) (string, error) {
return c.client.GetBucketPolicy(bucketName)
}
// MCS3Client interface with all functions to be implemented
// by mock when testing, it should include all mc/S3Client respective api calls
// that are used within this project.
type MCS3Client interface {
addNotificationConfig(arn string, events []string, prefix, suffix string, ignoreExisting bool) *probe.Error
removeNotificationConfig(arn string, event string, prefix string, suffix string) *probe.Error
}
// Interface implementation
//
// Define the structure of a mc S3Client and define the functions that are actually used
// from mcS3client api.
type mcS3Client struct {
client *mc.S3Client
}
// implements S3Client.AddNotificationConfig()
func (c mcS3Client) addNotificationConfig(arn string, events []string, prefix, suffix string, ignoreExisting bool) *probe.Error {
return c.client.AddNotificationConfig(arn, events, prefix, suffix, ignoreExisting)
}
// implements S3Client.RemoveNotificationConfig()
func (c mcS3Client) removeNotificationConfig(arn string, event string, prefix string, suffix string) *probe.Error {
return c.client.RemoveNotificationConfig(arn, event, prefix, suffix)
}
// MCSCredentials interface with all functions to be implemented
// by mock when testing, it should include all needed minioCredentials.Credentials api calls
// that are used within this project.
type MCSCredentials interface {
Get() (credentials.Value, error)
Expire()
}
// Interface implementation
type mcsCredentials struct {
minioCredentials *credentials.Credentials
}
// implements *Credentials.Get()
func (c mcsCredentials) Get() (credentials.Value, error) {
return c.minioCredentials.Get()
}
// implements *Credentials.Expire()
func (c mcsCredentials) Expire() {
c.minioCredentials.Expire()
}
// mcsSTSAssumeRole it's a STSAssumeRole wrapper, in general
// there's no need to use this struct anywhere else in the project, it's only required
// for passing a custom *http.Client to *credentials.STSAssumeRole
type mcsSTSAssumeRole struct {
stsAssumeRole *credentials.STSAssumeRole
}
func (s mcsSTSAssumeRole) Retrieve() (credentials.Value, error) {
return s.stsAssumeRole.Retrieve()
}
func (s mcsSTSAssumeRole) IsExpired() bool {
return s.stsAssumeRole.IsExpired()
}
// STSClient contains http.client configuration need it by STSAssumeRole
var STSClient = PrepareSTSClient()
func newMcsCredentials(accessKey, secretKey, location string) (*credentials.Credentials, error) {
mcsEndpoint := getMinIOServer()
if mcsEndpoint == "" {
return nil, errors.New("STS endpoint cannot be empty")
}
if accessKey == "" || secretKey == "" {
return nil, errors.New("AssumeRole credentials access/secretkey is mandatory")
}
// Future authentication methods can be added under this switch statement
switch {
// LDAP authentication for MCS
case ldap.GetLDAPEnabled():
{
creds, err := auth.GetMcsCredentialsFromLDAP(mcsEndpoint, accessKey, secretKey)
if err != nil {
return nil, err
}
return creds, nil
}
// default authentication for MCS is via STS (Security Token Service) against MinIO
default:
{
opts := credentials.STSAssumeRoleOptions{
AccessKey: accessKey,
SecretKey: secretKey,
Location: location,
DurationSeconds: xjwt.GetMcsSTSAndJWTDurationInSeconds(),
}
stsAssumeRole := &credentials.STSAssumeRole{
Client: STSClient,
STSEndpoint: mcsEndpoint,
Options: opts,
}
mcsSTSWrapper := mcsSTSAssumeRole{stsAssumeRole: stsAssumeRole}
return credentials.New(mcsSTSWrapper), nil
}
}
}
// getMcsCredentialsFromJWT returns the *minioCredentials.Credentials associated to the
// provided jwt, this is useful for running the Expire() or IsExpired() operations
func getMcsCredentialsFromJWT(jwt string) (*credentials.Credentials, error) {
claims, err := auth.JWTAuthenticate(jwt)
if err != nil {
return nil, err
}
creds := credentials.NewStaticV4(claims.AccessKeyID, claims.SecretAccessKey, claims.SessionToken)
return creds, nil
}
// newMinioClient creates a new MinIO client based on the minioCredentials extracted
// from the provided jwt
func newMinioClient(jwt string) (*minio.Client, error) {
creds, err := getMcsCredentialsFromJWT(jwt)
if err != nil {
return nil, err
}
minioClient, err := minio.NewWithOptions(getMinIOEndpoint(), &minio.Options{
Creds: creds,
Secure: getMinIOEndpointIsSecure(),
})
if err != nil {
return nil, err
}
minioClient.SetCustomTransport(STSClient.Transport)
return minioClient, nil
}
// newS3BucketClient creates a new mc S3Client to talk to the server based on a bucket
func newS3BucketClient(bucketName *string) (*mc.S3Client, error) {
endpoint := getMinIOServer()
accessKeyID := getAccessKey()
secretAccessKey := getSecretKey()
useSSL := getMinIOEndpointIsSecure()
if bucketName != nil {
endpoint += fmt.Sprintf("/%s", *bucketName)
}
s3Config := newS3Config(endpoint, accessKeyID, secretAccessKey, !useSSL)
client, err := mc.S3New(s3Config)
if err != nil {
return nil, err.Cause
}
s3Client, ok := client.(*mc.S3Client)
if !ok {
return nil, fmt.Errorf("the provided url doesn't point to a S3 server")
}
return s3Client, nil
}
// newS3Config simply creates a new Config struct using the passed
// parameters.
func newS3Config(endpoint, accessKey, secretKey string, isSecure bool) *mc.Config {
// We have a valid alias and hostConfig. We populate the
// minioCredentials from the match found in the config file.
s3Config := new(mc.Config)
s3Config.AppName = "mcs" // TODO: make this a constant
s3Config.AppVersion = "" // TODO: get this from constant or build
s3Config.AppComments = []string{}
s3Config.Debug = false
s3Config.Insecure = isSecure
s3Config.HostURL = endpoint
s3Config.AccessKey = accessKey
s3Config.SecretKey = secretKey
s3Config.Signature = "S3v4"
return s3Config
}