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 ```
268 lines
8.9 KiB
Go
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
|
|
}
|