diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md
new file mode 100644
index 000000000..5b55e33e6
--- /dev/null
+++ b/DEVELOPMENT.md
@@ -0,0 +1,127 @@
+# LDAP authentication with MCS
+
+## Setup
+
+Run openLDAP with docker.
+
+```
+$ 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
+# LDIF fragment to create group branch under root
+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
+# Create base group
+dn: ou=groups,dc=example,dc=org
+objectclass:organizationalunit
+ou: groups
+description: generic groups branch
+# create mcsAdmin group (this already exists on minio and have a policy of s3::*)
+dn: cn=mcsAdmin,ou=groups,dc=example,dc=org
+objectClass: top
+objectClass: posixGroup
+gidNumber: 678
+# Assing group to new user
+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
+```
diff --git a/go.sum b/go.sum
index 3aa020e58..f5bb9e37a 100644
--- a/go.sum
+++ b/go.sum
@@ -115,6 +115,7 @@ github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32/go.mod h1:GIjDIg/heH
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-ldap/ldap v3.0.2+incompatible h1:kD5HQcAzlQ7yrhfn+h+MSABeAy/jAJhvIJ/QDllP44g=
github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
diff --git a/pkg/auth/ldap.go b/pkg/auth/ldap.go
new file mode 100644
index 000000000..f2861cce5
--- /dev/null
+++ b/pkg/auth/ldap.go
@@ -0,0 +1,39 @@
+// This file is part of MinIO Console Server
+// 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 .
+
+package auth
+
+import (
+ "errors"
+ "log"
+
+ "github.com/minio/minio-go/v6/pkg/credentials"
+)
+
+var (
+ errInvalidCredentials = errors.New("invalid Credentials")
+)
+
+// GetMcsCredentialsFromLDAP authenticates the user against MinIO when the LDAP integration is enabled
+// if the authentication succeed *credentials.Credentials object is returned and we continue with the normal STSAssumeRole flow
+func GetMcsCredentialsFromLDAP(endpoint, ldapUser, ldapPassword string) (*credentials.Credentials, error) {
+ creds, err := credentials.NewLDAPIdentity(endpoint, ldapUser, ldapPassword)
+ if err != nil {
+ log.Println("LDAP authentication error: ", err)
+ return nil, errInvalidCredentials
+ }
+ return creds, nil
+}
diff --git a/pkg/auth/ldap/config.go b/pkg/auth/ldap/config.go
new file mode 100644
index 000000000..703d2469c
--- /dev/null
+++ b/pkg/auth/ldap/config.go
@@ -0,0 +1,27 @@
+// This file is part of MinIO Console Server
+// 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 .
+
+package ldap
+
+import (
+ "strings"
+
+ "github.com/minio/minio/pkg/env"
+)
+
+func GetLDAPEnabled() bool {
+ return strings.ToLower(env.Get(MCSLDAPEnabled, "off")) == "on"
+}
diff --git a/pkg/auth/ldap/const.go b/pkg/auth/ldap/const.go
new file mode 100644
index 000000000..1afc1c7c8
--- /dev/null
+++ b/pkg/auth/ldap/const.go
@@ -0,0 +1,22 @@
+// This file is part of MinIO Console Server
+// 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 .
+
+package ldap
+
+const (
+ // const for ldap configuration
+ MCSLDAPEnabled = "MCS_LDAP_ENABLED"
+)
diff --git a/restapi/client.go b/restapi/client.go
index f77e492a0..6ee63bed3 100644
--- a/restapi/client.go
+++ b/restapi/client.go
@@ -18,13 +18,15 @@ package restapi
import (
"context"
- "errors"
"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"
)
@@ -153,26 +155,43 @@ func (s mcsSTSAssumeRole) IsExpired() bool {
var STSClient = PrepareSTSClient()
func newMcsCredentials(accessKey, secretKey, location string) (*credentials.Credentials, error) {
- stsEndpoint := getMinIOServer()
- if stsEndpoint == "" {
+ 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")
}
- opts := credentials.STSAssumeRoleOptions{
- AccessKey: accessKey,
- SecretKey: secretKey,
- Location: location,
- DurationSeconds: xjwt.GetMcsSTSAndJWTDurationInSeconds(),
+
+ // 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
+ }
}
- stsAssumeRole := &credentials.STSAssumeRole{
- Client: STSClient,
- STSEndpoint: stsEndpoint,
- Options: opts,
- }
- mcsSTSWrapper := mcsSTSAssumeRole{stsAssumeRole: stsAssumeRole}
- return credentials.New(mcsSTSWrapper), nil
}
// getMcsCredentialsFromJWT returns the *minioCredentials.Credentials associated to the
diff --git a/restapi/user_login.go b/restapi/user_login.go
index 9dfbbc339..aa5ceac7c 100644
--- a/restapi/user_login.go
+++ b/restapi/user_login.go
@@ -49,14 +49,14 @@ func registerLoginHandlers(api *operations.McsAPI) {
api.UserAPILoginHandler = user_api.LoginHandlerFunc(func(params user_api.LoginParams) middleware.Responder {
loginResponse, err := getLoginResponse(params.Body)
if err != nil {
- return user_api.NewLoginDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
+ return user_api.NewLoginDefault(401).WithPayload(&models.Error{Code: 401, Message: swag.String(err.Error())})
}
return user_api.NewLoginCreated().WithPayload(loginResponse)
})
api.UserAPILoginOauth2AuthHandler = user_api.LoginOauth2AuthHandlerFunc(func(params user_api.LoginOauth2AuthParams) middleware.Responder {
loginResponse, err := getLoginOauth2AuthResponse(params.Body)
if err != nil {
- return user_api.NewLoginOauth2AuthDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
+ return user_api.NewLoginOauth2AuthDefault(401).WithPayload(&models.Error{Code: 401, Message: swag.String(err.Error())})
}
return user_api.NewLoginOauth2AuthCreated().WithPayload(loginResponse)
})