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) })