mirror of
https://github.com/minio/minio.git
synced 2024-12-23 21:55:53 -05:00
feat: Add support to poll users on external SSO (#12592)
Additional support for vendor-specific admin API integrations for OpenID, to ensure validity of credentials on MinIO. Every 5minutes check for validity of credentials on MinIO with vendor specific IDP.
This commit is contained in:
parent
b79cdc1611
commit
28adb29db3
@ -162,7 +162,7 @@ func (ies *IAMEtcdStore) migrateUsersConfigToV1(ctx context.Context) error {
|
||||
|
||||
// 2. copy policy to new loc.
|
||||
mp := newMappedPolicy(policyName)
|
||||
userType := regularUser
|
||||
userType := regUser
|
||||
path := getMappedPolicyPath(user, userType, false)
|
||||
if err := ies.saveIAMConfig(ctx, mp, path); err != nil {
|
||||
return err
|
||||
@ -335,7 +335,7 @@ func (ies *IAMEtcdStore) loadUser(ctx context.Context, user string, userType IAM
|
||||
func (ies *IAMEtcdStore) loadUsers(ctx context.Context, userType IAMUserType, m map[string]auth.Credentials) error {
|
||||
var basePrefix string
|
||||
switch userType {
|
||||
case srvAccUser:
|
||||
case svcUser:
|
||||
basePrefix = iamConfigServiceAccountsPrefix
|
||||
case stsUser:
|
||||
basePrefix = iamConfigSTSPrefix
|
||||
@ -432,7 +432,7 @@ func (ies *IAMEtcdStore) loadMappedPolicies(ctx context.Context, userType IAMUse
|
||||
basePrefix = iamConfigPolicyDBGroupsPrefix
|
||||
} else {
|
||||
switch userType {
|
||||
case srvAccUser:
|
||||
case svcUser:
|
||||
basePrefix = iamConfigPolicyDBServiceAccountsPrefix
|
||||
case stsUser:
|
||||
basePrefix = iamConfigPolicyDBSTSUsersPrefix
|
||||
@ -567,7 +567,7 @@ func (ies *IAMEtcdStore) reloadFromEvent(sys *IAMSys, event *etcd.Event) {
|
||||
case usersPrefix:
|
||||
accessKey := path.Dir(strings.TrimPrefix(string(event.Kv.Key),
|
||||
iamConfigUsersPrefix))
|
||||
ies.loadUser(ctx, accessKey, regularUser, sys.iamUsersMap)
|
||||
ies.loadUser(ctx, accessKey, regUser, sys.iamUsersMap)
|
||||
case stsPrefix:
|
||||
accessKey := path.Dir(strings.TrimPrefix(string(event.Kv.Key),
|
||||
iamConfigSTSPrefix))
|
||||
@ -593,7 +593,7 @@ func (ies *IAMEtcdStore) reloadFromEvent(sys *IAMSys, event *etcd.Event) {
|
||||
case svcPrefix:
|
||||
accessKey := path.Dir(strings.TrimPrefix(string(event.Kv.Key),
|
||||
iamConfigServiceAccountsPrefix))
|
||||
ies.loadUser(ctx, accessKey, srvAccUser, sys.iamUsersMap)
|
||||
ies.loadUser(ctx, accessKey, svcUser, sys.iamUsersMap)
|
||||
case groupsPrefix:
|
||||
group := path.Dir(strings.TrimPrefix(string(event.Kv.Key),
|
||||
iamConfigGroupsPrefix))
|
||||
@ -609,7 +609,7 @@ func (ies *IAMEtcdStore) reloadFromEvent(sys *IAMSys, event *etcd.Event) {
|
||||
policyMapFile := strings.TrimPrefix(string(event.Kv.Key),
|
||||
iamConfigPolicyDBUsersPrefix)
|
||||
user := strings.TrimSuffix(policyMapFile, ".json")
|
||||
ies.loadMappedPolicy(ctx, user, regularUser, false, sys.iamUserPolicyMap)
|
||||
ies.loadMappedPolicy(ctx, user, regUser, false, sys.iamUserPolicyMap)
|
||||
case policyDBSTSUsersPrefix:
|
||||
policyMapFile := strings.TrimPrefix(string(event.Kv.Key),
|
||||
iamConfigPolicyDBSTSUsersPrefix)
|
||||
@ -619,7 +619,7 @@ func (ies *IAMEtcdStore) reloadFromEvent(sys *IAMSys, event *etcd.Event) {
|
||||
policyMapFile := strings.TrimPrefix(string(event.Kv.Key),
|
||||
iamConfigPolicyDBGroupsPrefix)
|
||||
user := strings.TrimSuffix(policyMapFile, ".json")
|
||||
ies.loadMappedPolicy(ctx, user, regularUser, true, sys.iamGroupPolicyMap)
|
||||
ies.loadMappedPolicy(ctx, user, regUser, true, sys.iamGroupPolicyMap)
|
||||
}
|
||||
case eventDelete:
|
||||
switch {
|
||||
|
@ -104,7 +104,7 @@ func (iamOS *IAMObjectStore) migrateUsersConfigToV1(ctx context.Context) error {
|
||||
|
||||
// 2. copy policy file to new location.
|
||||
mp := newMappedPolicy(policyName)
|
||||
userType := regularUser
|
||||
userType := regUser
|
||||
if err := iamOS.saveMappedPolicy(ctx, user, userType, false, mp); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -279,7 +279,7 @@ func (iamOS *IAMObjectStore) loadUser(ctx context.Context, user string, userType
|
||||
func (iamOS *IAMObjectStore) loadUsers(ctx context.Context, userType IAMUserType, m map[string]auth.Credentials) error {
|
||||
var basePrefix string
|
||||
switch userType {
|
||||
case srvAccUser:
|
||||
case svcUser:
|
||||
basePrefix = iamConfigServiceAccountsPrefix
|
||||
case stsUser:
|
||||
basePrefix = iamConfigSTSPrefix
|
||||
@ -348,7 +348,7 @@ func (iamOS *IAMObjectStore) loadMappedPolicies(ctx context.Context, userType IA
|
||||
basePath = iamConfigPolicyDBGroupsPrefix
|
||||
} else {
|
||||
switch userType {
|
||||
case srvAccUser:
|
||||
case svcUser:
|
||||
basePath = iamConfigPolicyDBServiceAccountsPrefix
|
||||
case stsUser:
|
||||
basePath = iamConfigPolicyDBSTSUsersPrefix
|
||||
|
142
cmd/iam.go
142
cmd/iam.go
@ -112,7 +112,7 @@ func getIAMFormatFilePath() string {
|
||||
func getUserIdentityPath(user string, userType IAMUserType) string {
|
||||
var basePath string
|
||||
switch userType {
|
||||
case srvAccUser:
|
||||
case svcUser:
|
||||
basePath = iamConfigServiceAccountsPrefix
|
||||
case stsUser:
|
||||
basePath = iamConfigSTSPrefix
|
||||
@ -135,7 +135,7 @@ func getMappedPolicyPath(name string, userType IAMUserType, isGroup bool) string
|
||||
return pathJoin(iamConfigPolicyDBGroupsPrefix, name+".json")
|
||||
}
|
||||
switch userType {
|
||||
case srvAccUser:
|
||||
case svcUser:
|
||||
return pathJoin(iamConfigPolicyDBServiceAccountsPrefix, name+".json")
|
||||
case stsUser:
|
||||
return pathJoin(iamConfigPolicyDBSTSUsersPrefix, name+".json")
|
||||
@ -230,9 +230,9 @@ type IAMSys struct {
|
||||
type IAMUserType int
|
||||
|
||||
const (
|
||||
regularUser IAMUserType = iota
|
||||
regUser IAMUserType = iota
|
||||
stsUser
|
||||
srvAccUser
|
||||
svcUser
|
||||
)
|
||||
|
||||
// key options
|
||||
@ -355,7 +355,7 @@ func (sys *IAMSys) LoadPolicyMapping(objAPI ObjectLayer, userOrGroup string, isG
|
||||
|
||||
if globalEtcdClient == nil {
|
||||
var err error
|
||||
userType := regularUser
|
||||
userType := regUser
|
||||
if sys.usersSysType == LDAPUsersSysType {
|
||||
userType = stsUser
|
||||
}
|
||||
@ -432,7 +432,7 @@ func (sys *IAMSys) LoadServiceAccount(accessKey string) error {
|
||||
defer sys.store.unlock()
|
||||
|
||||
if globalEtcdClient == nil {
|
||||
err := sys.store.loadUser(context.Background(), accessKey, srvAccUser, sys.iamUsersMap)
|
||||
err := sys.store.loadUser(context.Background(), accessKey, svcUser, sys.iamUsersMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -492,7 +492,7 @@ func (sys *IAMSys) Load(ctx context.Context, store IAMStorageAPI) error {
|
||||
setDefaultCannedPolicies(iamPolicyDocsMap)
|
||||
|
||||
if isMinIOUsersSys {
|
||||
if err := store.loadUsers(ctx, regularUser, iamUsersMap); err != nil {
|
||||
if err := store.loadUsers(ctx, regUser, iamUsersMap); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := store.loadGroups(ctx, iamGroupsMap); err != nil {
|
||||
@ -501,16 +501,16 @@ func (sys *IAMSys) Load(ctx context.Context, store IAMStorageAPI) error {
|
||||
}
|
||||
|
||||
// load polices mapped to users
|
||||
if err := store.loadMappedPolicies(ctx, regularUser, false, iamUserPolicyMap); err != nil {
|
||||
if err := store.loadMappedPolicies(ctx, regUser, false, iamUserPolicyMap); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// load policies mapped to groups
|
||||
if err := store.loadMappedPolicies(ctx, regularUser, true, iamGroupPolicyMap); err != nil {
|
||||
if err := store.loadMappedPolicies(ctx, regUser, true, iamGroupPolicyMap); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := store.loadUsers(ctx, srvAccUser, iamUsersMap); err != nil {
|
||||
if err := store.loadUsers(ctx, svcUser, iamUsersMap); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -545,33 +545,11 @@ func (sys *IAMSys) Load(ctx context.Context, store IAMStorageAPI) error {
|
||||
}
|
||||
|
||||
// purge any expired entries which became expired now.
|
||||
var expiredEntries []string
|
||||
for k, v := range sys.iamUsersMap {
|
||||
if v.IsExpired() {
|
||||
delete(sys.iamUsersMap, k)
|
||||
delete(sys.iamUserPolicyMap, k)
|
||||
expiredEntries = append(expiredEntries, k)
|
||||
// Deleting on the disk is taken care of in the next cycle
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range sys.iamUsersMap {
|
||||
if v.IsServiceAccount() {
|
||||
for _, accessKey := range expiredEntries {
|
||||
if v.ParentUser == accessKey {
|
||||
_ = store.deleteUserIdentity(ctx, v.AccessKey, srvAccUser)
|
||||
delete(sys.iamUsersMap, v.AccessKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// purge any expired entries which became expired now.
|
||||
for k, v := range sys.iamUsersMap {
|
||||
if v.IsExpired() {
|
||||
delete(sys.iamUsersMap, k)
|
||||
delete(sys.iamUserPolicyMap, k)
|
||||
// Deleting on the etcd is taken care of in the next cycle
|
||||
// deleting will be done in the next cycle.
|
||||
}
|
||||
}
|
||||
|
||||
@ -668,6 +646,16 @@ func (sys *IAMSys) Init(ctx context.Context, objAPI ObjectLayer) {
|
||||
break
|
||||
}
|
||||
|
||||
if globalOpenIDConfig.ProviderEnabled() {
|
||||
go func() {
|
||||
// Purge expired credentials
|
||||
for {
|
||||
time.Sleep(globalRefreshIAMInterval)
|
||||
sys.purgeExpiredCredentialsForExternalSSO(ctx)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
go sys.store.watch(ctx, sys)
|
||||
}
|
||||
|
||||
@ -708,7 +696,7 @@ func (sys *IAMSys) DeletePolicy(policyName string) error {
|
||||
if cr.IsTemp() {
|
||||
sys.policyDBSet(u, strings.Join(pset.ToSlice(), ","), stsUser, false)
|
||||
} else {
|
||||
sys.policyDBSet(u, strings.Join(pset.ToSlice(), ","), regularUser, false)
|
||||
sys.policyDBSet(u, strings.Join(pset.ToSlice(), ","), regUser, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -718,7 +706,7 @@ func (sys *IAMSys) DeletePolicy(policyName string) error {
|
||||
pset := mp.policySet()
|
||||
if pset.Contains(policyName) {
|
||||
pset.Remove(policyName)
|
||||
sys.policyDBSet(g, strings.Join(pset.ToSlice(), ","), regularUser, true)
|
||||
sys.policyDBSet(g, strings.Join(pset.ToSlice(), ","), regUser, true)
|
||||
}
|
||||
}
|
||||
|
||||
@ -823,7 +811,7 @@ func (sys *IAMSys) DeleteUser(accessKey string) error {
|
||||
// Delete any service accounts if any first.
|
||||
if u.IsServiceAccount() {
|
||||
if u.ParentUser == accessKey {
|
||||
_ = sys.store.deleteUserIdentity(context.Background(), u.AccessKey, srvAccUser)
|
||||
_ = sys.store.deleteUserIdentity(context.Background(), u.AccessKey, svcUser)
|
||||
delete(sys.iamUsersMap, u.AccessKey)
|
||||
}
|
||||
}
|
||||
@ -837,8 +825,8 @@ func (sys *IAMSys) DeleteUser(accessKey string) error {
|
||||
}
|
||||
|
||||
// It is ok to ignore deletion error on the mapped policy
|
||||
sys.store.deleteMappedPolicy(context.Background(), accessKey, regularUser, false)
|
||||
err := sys.store.deleteUserIdentity(context.Background(), accessKey, regularUser)
|
||||
sys.store.deleteMappedPolicy(context.Background(), accessKey, regUser, false)
|
||||
err := sys.store.deleteUserIdentity(context.Background(), accessKey, regUser)
|
||||
if err == errNoSuchUser {
|
||||
// ignore if user is already deleted.
|
||||
err = nil
|
||||
@ -1153,7 +1141,7 @@ func (sys *IAMSys) SetUserStatus(accessKey string, status madmin.AccountStatus)
|
||||
}(),
|
||||
})
|
||||
|
||||
if err := sys.store.saveUserIdentity(context.Background(), accessKey, regularUser, uinfo); err != nil {
|
||||
if err := sys.store.saveUserIdentity(context.Background(), accessKey, regUser, uinfo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -1240,7 +1228,7 @@ func (sys *IAMSys) NewServiceAccount(ctx context.Context, parentUser string, gro
|
||||
|
||||
u := newUserIdentity(cred)
|
||||
|
||||
if err := sys.store.saveUserIdentity(context.Background(), u.Credentials.AccessKey, srvAccUser, u); err != nil {
|
||||
if err := sys.store.saveUserIdentity(context.Background(), u.Credentials.AccessKey, svcUser, u); err != nil {
|
||||
return auth.Credentials{}, err
|
||||
}
|
||||
|
||||
@ -1310,7 +1298,7 @@ func (sys *IAMSys) UpdateServiceAccount(ctx context.Context, accessKey string, o
|
||||
}
|
||||
|
||||
u := newUserIdentity(cr)
|
||||
if err := sys.store.saveUserIdentity(context.Background(), u.Credentials.AccessKey, srvAccUser, u); err != nil {
|
||||
if err := sys.store.saveUserIdentity(context.Background(), u.Credentials.AccessKey, svcUser, u); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -1397,7 +1385,7 @@ func (sys *IAMSys) DeleteServiceAccount(ctx context.Context, accessKey string) e
|
||||
}
|
||||
|
||||
// It is ok to ignore deletion error on the mapped policy
|
||||
err := sys.store.deleteUserIdentity(context.Background(), accessKey, srvAccUser)
|
||||
err := sys.store.deleteUserIdentity(context.Background(), accessKey, svcUser)
|
||||
if err != nil {
|
||||
// ignore if user is already deleted.
|
||||
if err == errNoSuchUser {
|
||||
@ -1448,7 +1436,7 @@ func (sys *IAMSys) CreateUser(accessKey string, uinfo madmin.UserInfo) error {
|
||||
}(),
|
||||
})
|
||||
|
||||
if err := sys.store.saveUserIdentity(context.Background(), accessKey, regularUser, u); err != nil {
|
||||
if err := sys.store.saveUserIdentity(context.Background(), accessKey, regUser, u); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -1456,7 +1444,7 @@ func (sys *IAMSys) CreateUser(accessKey string, uinfo madmin.UserInfo) error {
|
||||
|
||||
// Set policy if specified.
|
||||
if uinfo.PolicyName != "" {
|
||||
return sys.policyDBSet(accessKey, uinfo.PolicyName, regularUser, false)
|
||||
return sys.policyDBSet(accessKey, uinfo.PolicyName, regUser, false)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -1489,7 +1477,7 @@ func (sys *IAMSys) SetUserSecretKey(accessKey string, secretKey string) error {
|
||||
|
||||
cred.SecretKey = secretKey
|
||||
u := newUserIdentity(cred)
|
||||
if err := sys.store.saveUserIdentity(context.Background(), accessKey, regularUser, u); err != nil {
|
||||
if err := sys.store.saveUserIdentity(context.Background(), accessKey, regUser, u); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -1501,18 +1489,18 @@ func (sys *IAMSys) loadUserFromStore(accessKey string) {
|
||||
sys.store.lock()
|
||||
// If user is already found proceed.
|
||||
if _, found := sys.iamUsersMap[accessKey]; !found {
|
||||
sys.store.loadUser(context.Background(), accessKey, regularUser, sys.iamUsersMap)
|
||||
sys.store.loadUser(context.Background(), accessKey, regUser, sys.iamUsersMap)
|
||||
if _, found = sys.iamUsersMap[accessKey]; found {
|
||||
// found user, load its mapped policies
|
||||
sys.store.loadMappedPolicy(context.Background(), accessKey, regularUser, false, sys.iamUserPolicyMap)
|
||||
sys.store.loadMappedPolicy(context.Background(), accessKey, regUser, false, sys.iamUserPolicyMap)
|
||||
} else {
|
||||
sys.store.loadUser(context.Background(), accessKey, srvAccUser, sys.iamUsersMap)
|
||||
sys.store.loadUser(context.Background(), accessKey, svcUser, sys.iamUsersMap)
|
||||
if svc, found := sys.iamUsersMap[accessKey]; found {
|
||||
// Found service account, load its parent user and its mapped policies.
|
||||
if sys.usersSysType == MinIOUsersSysType {
|
||||
sys.store.loadUser(context.Background(), svc.ParentUser, regularUser, sys.iamUsersMap)
|
||||
sys.store.loadUser(context.Background(), svc.ParentUser, regUser, sys.iamUsersMap)
|
||||
}
|
||||
sys.store.loadMappedPolicy(context.Background(), svc.ParentUser, regularUser, false, sys.iamUserPolicyMap)
|
||||
sys.store.loadMappedPolicy(context.Background(), svc.ParentUser, regUser, false, sys.iamUserPolicyMap)
|
||||
} else {
|
||||
// None found fall back to STS users.
|
||||
sys.store.loadUser(context.Background(), accessKey, stsUser, sys.iamUsersMap)
|
||||
@ -1535,6 +1523,54 @@ func (sys *IAMSys) loadUserFromStore(accessKey string) {
|
||||
sys.store.unlock()
|
||||
}
|
||||
|
||||
// purgeExpiredCredentialsForExternalSSO - validates if local credentials are still valid
|
||||
// by checking remote IDP if the relevant users are still active and present.
|
||||
func (sys *IAMSys) purgeExpiredCredentialsForExternalSSO(ctx context.Context) {
|
||||
sys.store.lock()
|
||||
parentUsersMap := make(map[string]auth.Credentials, len(sys.iamUsersMap))
|
||||
for _, cred := range sys.iamUsersMap {
|
||||
if cred.IsServiceAccount() || cred.IsTemp() {
|
||||
userid, err := parseOpenIDParentUser(cred.ParentUser)
|
||||
if err == errSkipFile {
|
||||
continue
|
||||
}
|
||||
parentUsersMap[userid] = cred
|
||||
}
|
||||
}
|
||||
sys.store.unlock()
|
||||
|
||||
expiredUsers := make([]auth.Credentials, 0, len(parentUsersMap))
|
||||
for userid, cred := range parentUsersMap {
|
||||
u, err := globalOpenIDConfig.LookupUser(userid)
|
||||
if err != nil {
|
||||
logger.LogIf(GlobalContext, err)
|
||||
continue
|
||||
}
|
||||
// Disabled parentUser purge the entries locally
|
||||
if !u.Enabled {
|
||||
expiredUsers = append(expiredUsers, cred)
|
||||
}
|
||||
}
|
||||
|
||||
for _, cred := range expiredUsers {
|
||||
userType := regUser
|
||||
if cred.IsServiceAccount() {
|
||||
userType = svcUser
|
||||
} else if cred.IsTemp() {
|
||||
userType = stsUser
|
||||
}
|
||||
sys.store.deleteIAMConfig(ctx, getUserIdentityPath(cred.AccessKey, userType))
|
||||
sys.store.deleteIAMConfig(ctx, getMappedPolicyPath(cred.AccessKey, userType, false))
|
||||
}
|
||||
|
||||
sys.store.lock()
|
||||
for _, cred := range expiredUsers {
|
||||
delete(sys.iamUsersMap, cred.AccessKey)
|
||||
delete(sys.iamUserPolicyMap, cred.AccessKey)
|
||||
}
|
||||
sys.store.unlock()
|
||||
}
|
||||
|
||||
// GetUser - get user credentials
|
||||
func (sys *IAMSys) GetUser(accessKey string) (cred auth.Credentials, ok bool) {
|
||||
if !sys.Initialized() {
|
||||
@ -1693,7 +1729,7 @@ func (sys *IAMSys) RemoveUsersFromGroup(group string, members []string) error {
|
||||
|
||||
// Remove the group from storage. First delete the
|
||||
// mapped policy. No-mapped-policy case is ignored.
|
||||
if err := sys.store.deleteMappedPolicy(context.Background(), group, regularUser, true); err != nil && err != errNoSuchPolicy {
|
||||
if err := sys.store.deleteMappedPolicy(context.Background(), group, regUser, true); err != nil && err != errNoSuchPolicy {
|
||||
return err
|
||||
}
|
||||
if err := sys.store.deleteGroupInfo(context.Background(), group); err != nil && err != errNoSuchGroup {
|
||||
@ -1839,7 +1875,7 @@ func (sys *IAMSys) PolicyDBSet(name, policy string, isGroup bool) error {
|
||||
return sys.policyDBSet(name, policy, stsUser, isGroup)
|
||||
}
|
||||
|
||||
return sys.policyDBSet(name, policy, regularUser, isGroup)
|
||||
return sys.policyDBSet(name, policy, regUser, isGroup)
|
||||
}
|
||||
|
||||
// policyDBSet - sets a policy for user in the policy db. Assumes that caller
|
||||
@ -1867,7 +1903,7 @@ func (sys *IAMSys) policyDBSet(name, policyName string, userType IAMUserType, is
|
||||
// Add a fallback removal towards previous content that may come back
|
||||
// as a ghost user due to lack of delete, this change occurred
|
||||
// introduced in PR #11840
|
||||
sys.store.deleteMappedPolicy(context.Background(), name, regularUser, false)
|
||||
sys.store.deleteMappedPolicy(context.Background(), name, regUser, false)
|
||||
}
|
||||
err := sys.store.deleteMappedPolicy(context.Background(), name, userType, isGroup)
|
||||
if err != nil && err != errNoSuchPolicy {
|
||||
|
@ -249,7 +249,7 @@ func (s *peerRESTServer) LoadUserHandler(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
var userType = regularUser
|
||||
var userType = regUser
|
||||
if temp {
|
||||
userType = stsUser
|
||||
}
|
||||
|
@ -68,6 +68,16 @@ const (
|
||||
ldapUsername = "ldapUsername"
|
||||
)
|
||||
|
||||
func parseOpenIDParentUser(parentUser string) (userID string, err error) {
|
||||
if strings.HasPrefix(parentUser, "jwt:") {
|
||||
tokens := strings.SplitN(strings.TrimPrefix(parentUser, "jwt:"), ":", 2)
|
||||
if len(tokens) == 2 {
|
||||
return tokens[0], nil
|
||||
}
|
||||
}
|
||||
return "", errSkipFile
|
||||
}
|
||||
|
||||
// stsAPIHandlers implements and provides http handlers for AWS STS API.
|
||||
type stsAPIHandlers struct{}
|
||||
|
||||
|
@ -6,12 +6,15 @@ Keycloak is an open source Identity and Access Management solution aimed at mode
|
||||
|
||||
Configure and install keycloak server by following [Keycloak Installation Guide](https://www.keycloak.org/docs/latest/getting_started/index.html) (finish upto section 3.4)
|
||||
|
||||
### Configure Keycloak UI
|
||||
### Configure Keycloak Realm
|
||||
- Go to Clients
|
||||
- Click on account
|
||||
- Settings
|
||||
- Enable `Implicit Flow`
|
||||
- Save
|
||||
- Settings
|
||||
- Change `Access Type` to `confidential`.
|
||||
- Save
|
||||
- Click on credentials tab
|
||||
- Copy the `Secret` to clipboard.
|
||||
- This value is needed for `MINIO_IDENTITY_OPENID_CLIENT_SECRET` for MinIO.
|
||||
|
||||
- Go to Users
|
||||
- Click on the user
|
||||
@ -36,6 +39,42 @@ Configure and install keycloak server by following [Keycloak Installation Guide]
|
||||
|
||||
- Open http://localhost:8080/auth/realms/minio/.well-known/openid-configuration to verify OpenID discovery document, verify it has `authorization_endpoint` and `jwks_uri`
|
||||
|
||||
### Enable Keycloak Admin REST API support
|
||||
Before being able to authenticate against the Admin REST API using a client_id and a client_secret you need to make sure the client is configured as it follows:
|
||||
|
||||
- `account` client_id is a confidential client that belongs to the realm `{realm}`
|
||||
- `account` client_id is has **Service Accounts Enabled** option enabled.
|
||||
- `account` client_id has a custom "Audience" mapper, in the Mappers section.
|
||||
- Included Client Audience: security-admin-console
|
||||
|
||||
#### Adding 'admin' Role
|
||||
|
||||
- Go to Roles
|
||||
- Add new Role `admin` with Description `${role_admin}`.
|
||||
- Add this Role into compositive role named `default-roles-{realm}` - `{realm}` should be replaced with whatever realm you created from `prerequisites` section. This role is automatically trusted in the 'Service Accounts' tab.
|
||||
|
||||
- Check that `account` client_id has the role 'admin' assigned in the "Service Account Roles" tab.
|
||||
|
||||
After that, you will be able to obtain an access token for the Admin REST API using client_id and client_secret:
|
||||
|
||||
```
|
||||
curl \
|
||||
-d "client_id=<YOUR_CLIENT_ID>" \
|
||||
-d "client_secret=<YOUR_CLIENT_SECRET>" \
|
||||
-d "grant_type=client_credentials" \
|
||||
"http://localhost:8080/auth/realms/{realm}/protocol/openid-connect/token"
|
||||
```
|
||||
|
||||
The result will be a JSON document. To invoke the API you need to extract the value of the access_token property. You can then invoke the API by including the value in the Authorization header of requests to the API.
|
||||
|
||||
The following example shows how to get the details of the user with `{userid}` from `{realm}` realm:
|
||||
|
||||
```
|
||||
curl \
|
||||
-H "Authorization: Bearer eyJhbGciOiJSUz..." \
|
||||
"http://localhost:8080/auth/admin/realms/{realm}/users/{userid}"
|
||||
```
|
||||
|
||||
### Configure MinIO
|
||||
```
|
||||
$ export MINIO_ROOT_USER=minio
|
||||
|
@ -62,6 +62,24 @@ var (
|
||||
Optional: true,
|
||||
Type: "csv",
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: Vendor,
|
||||
Description: `Specify vendor type for vendor specific behavior to checking validity of temporary credentials and service accounts on MinIO`,
|
||||
Optional: true,
|
||||
Type: "string",
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: KeyCloakRealm,
|
||||
Description: `Specify Keycloak 'realm' name, only honored if vendor was set to 'keycloak' as value, if no realm is specified 'master' is default`,
|
||||
Optional: true,
|
||||
Type: "string",
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: KeyCloakAdminURL,
|
||||
Description: `Specify Keycloak 'admin' REST API endpoint e.g. http://localhost:8080/auth/admin/`,
|
||||
Optional: true,
|
||||
Type: "string",
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: config.Comment,
|
||||
Description: config.DefaultComment,
|
||||
|
@ -32,6 +32,7 @@ import (
|
||||
jwtgo "github.com/golang-jwt/jwt"
|
||||
"github.com/minio/minio/internal/auth"
|
||||
"github.com/minio/minio/internal/config"
|
||||
"github.com/minio/minio/internal/config/identity/openid/provider"
|
||||
"github.com/minio/pkg/env"
|
||||
iampolicy "github.com/minio/pkg/iam/policy"
|
||||
xnet "github.com/minio/pkg/net"
|
||||
@ -50,12 +51,77 @@ type Config struct {
|
||||
DiscoveryDoc DiscoveryDoc
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
provider provider.Provider
|
||||
publicKeys map[string]crypto.PublicKey
|
||||
transport *http.Transport
|
||||
closeRespFn func(io.ReadCloser)
|
||||
mutex *sync.Mutex
|
||||
}
|
||||
|
||||
// LookupUser lookup userid for the provider
|
||||
func (r *Config) LookupUser(userid string) (provider.User, error) {
|
||||
if r.provider != nil {
|
||||
user, err := r.provider.LookupUser(userid)
|
||||
if err != nil && err != provider.ErrAccessTokenExpired {
|
||||
return user, err
|
||||
}
|
||||
if err == provider.ErrAccessTokenExpired {
|
||||
if err = r.provider.LoginWithClientID(r.ClientID, r.ClientSecret); err != nil {
|
||||
return user, err
|
||||
}
|
||||
user, err = r.provider.LookupUser(userid)
|
||||
}
|
||||
return user, err
|
||||
}
|
||||
// Without any specific logic for a provider, all accounts
|
||||
// are always enabled.
|
||||
return provider.User{ID: userid, Enabled: true}, nil
|
||||
}
|
||||
|
||||
const (
|
||||
keyCloakVendor = "keycloak"
|
||||
)
|
||||
|
||||
// InitializeProvider initializes if any additional vendor specific
|
||||
// information was provided, initialization will return an error
|
||||
// initial login fails.
|
||||
func (r *Config) InitializeProvider(kvs config.KVS) error {
|
||||
vendor := env.Get(EnvIdentityOpenIDVendor, kvs.Get(Vendor))
|
||||
if vendor == "" {
|
||||
return nil
|
||||
}
|
||||
switch vendor {
|
||||
case keyCloakVendor:
|
||||
adminURL := env.Get(EnvIdentityOpenIDKeyCloakAdminURL, kvs.Get(KeyCloakAdminURL))
|
||||
realm := env.Get(EnvIdentityOpenIDKeyCloakRealm, kvs.Get(KeyCloakRealm))
|
||||
return r.InitializeKeycloakProvider(adminURL, realm)
|
||||
default:
|
||||
return fmt.Errorf("Unsupport vendor %s", keyCloakVendor)
|
||||
}
|
||||
}
|
||||
|
||||
// ProviderEnabled returns true if any vendor specific provider is enabled.
|
||||
func (r *Config) ProviderEnabled() bool {
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
return r.provider != nil
|
||||
}
|
||||
|
||||
// InitializeKeycloakProvider - initializes keycloak provider
|
||||
func (r *Config) InitializeKeycloakProvider(adminURL, realm string) error {
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
|
||||
var err error
|
||||
r.provider, err = provider.KeyCloak(
|
||||
provider.WithAdminURL(adminURL),
|
||||
provider.WithOpenIDConfig(provider.DiscoveryDoc(r.DiscoveryDoc)),
|
||||
provider.WithTransport(r.transport),
|
||||
provider.WithRealm(realm),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// PopulatePublicKey - populates a new publickey from the JWKS URL.
|
||||
func (r *Config) PopulatePublicKey() error {
|
||||
r.mutex.Lock()
|
||||
@ -228,9 +294,15 @@ const (
|
||||
ClaimPrefix = "claim_prefix"
|
||||
ClientID = "client_id"
|
||||
ClientSecret = "client_secret"
|
||||
Vendor = "vendor"
|
||||
Scopes = "scopes"
|
||||
RedirectURI = "redirect_uri"
|
||||
|
||||
// Vendor specific ENV only enabled if the Vendor matches == "vendor"
|
||||
KeyCloakRealm = "keycloak_realm"
|
||||
KeyCloakAdminURL = "keycloak_admin_url"
|
||||
|
||||
EnvIdentityOpenIDVendor = "MINIO_IDENTITY_OPENID_VENDOR"
|
||||
EnvIdentityOpenIDClientID = "MINIO_IDENTITY_OPENID_CLIENT_ID"
|
||||
EnvIdentityOpenIDClientSecret = "MINIO_IDENTITY_OPENID_CLIENT_SECRET"
|
||||
EnvIdentityOpenIDJWKSURL = "MINIO_IDENTITY_OPENID_JWKS_URL"
|
||||
@ -239,6 +311,10 @@ const (
|
||||
EnvIdentityOpenIDClaimPrefix = "MINIO_IDENTITY_OPENID_CLAIM_PREFIX"
|
||||
EnvIdentityOpenIDRedirectURI = "MINIO_IDENTITY_OPENID_REDIRECT_URI"
|
||||
EnvIdentityOpenIDScopes = "MINIO_IDENTITY_OPENID_SCOPES"
|
||||
|
||||
// Vendor specific ENVs only enabled if the Vendor matches == "vendor"
|
||||
EnvIdentityOpenIDKeyCloakRealm = "MINIO_IDENTITY_OPENID_KEYCLOAK_REALM"
|
||||
EnvIdentityOpenIDKeyCloakAdminURL = "MINIO_IDENTITY_OPENID_KEYCLOAK_ADMIN_URL"
|
||||
)
|
||||
|
||||
// DiscoveryDoc - parses the output from openid-configuration
|
||||
@ -397,6 +473,10 @@ func LookupConfig(kvs config.KVS, transport *http.Transport, closeRespFn func(io
|
||||
return c, err
|
||||
}
|
||||
|
||||
if err = c.InitializeProvider(kvs); err != nil {
|
||||
return c, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
|
184
internal/config/identity/openid/provider/keycloak.go
Normal file
184
internal/config/identity/openid/provider/keycloak.go
Normal file
@ -0,0 +1,184 @@
|
||||
// Copyright (c) 2015-2021 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 provider
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Token - parses the output from IDP access token.
|
||||
type Token struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
Expiry int `json:"expires_in"`
|
||||
}
|
||||
|
||||
// KeycloakProvider implements Provider interface for KeyCloak Identity Provider.
|
||||
type KeycloakProvider struct {
|
||||
sync.Mutex
|
||||
|
||||
oeConfig DiscoveryDoc
|
||||
client http.Client
|
||||
adminURL string
|
||||
realm string
|
||||
|
||||
// internal value refreshed
|
||||
accessToken Token
|
||||
}
|
||||
|
||||
// LoginWithUser authenticates username/password, not needed for Keycloak
|
||||
func (k *KeycloakProvider) LoginWithUser(username, password string) error {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
|
||||
// LoginWithClientID is implemented by Keycloak service account support
|
||||
func (k *KeycloakProvider) LoginWithClientID(clientID, clientSecret string) error {
|
||||
values := url.Values{}
|
||||
values.Set("client_id", clientID)
|
||||
values.Set("client_secret", clientSecret)
|
||||
values.Set("grant_type", "client_credentials")
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, k.oeConfig.TokenEndpoint, strings.NewReader(values.Encode()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
resp, err := k.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var accessToken Token
|
||||
if err = json.NewDecoder(resp.Body).Decode(&accessToken); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
k.Lock()
|
||||
k.accessToken = accessToken
|
||||
k.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// LookupUser lookup user by their userid.
|
||||
func (k *KeycloakProvider) LookupUser(userid string) (User, error) {
|
||||
lookupUserID := k.adminURL + "/realms" + k.realm + "/users/" + userid
|
||||
req, err := http.NewRequest(http.MethodGet, lookupUserID, nil)
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
k.Lock()
|
||||
accessToken := k.accessToken
|
||||
k.Unlock()
|
||||
if accessToken.AccessToken == "" {
|
||||
return User{}, ErrAccessTokenExpired
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+accessToken.AccessToken)
|
||||
resp, err := k.client.Do(req)
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK, http.StatusPartialContent:
|
||||
var u User
|
||||
if err = json.NewDecoder(resp.Body).Decode(&u); err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
return u, nil
|
||||
case http.StatusNotFound:
|
||||
return User{
|
||||
ID: userid,
|
||||
Enabled: false,
|
||||
}, nil
|
||||
case http.StatusUnauthorized:
|
||||
return User{}, ErrAccessTokenExpired
|
||||
}
|
||||
return User{}, fmt.Errorf("Unable to lookup %s - keycloak user lookup returned %v", userid, resp.Status)
|
||||
}
|
||||
|
||||
// Option is a function type that accepts a pointer Target
|
||||
type Option func(*KeycloakProvider)
|
||||
|
||||
// WithTransport provide custom transport
|
||||
func WithTransport(transport *http.Transport) Option {
|
||||
return func(p *KeycloakProvider) {
|
||||
p.client = http.Client{
|
||||
Transport: transport,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithOpenIDConfig provide OpenID Endpoint configuration discovery document
|
||||
func WithOpenIDConfig(oeConfig DiscoveryDoc) Option {
|
||||
return func(p *KeycloakProvider) {
|
||||
p.oeConfig = oeConfig
|
||||
}
|
||||
}
|
||||
|
||||
// WithAdminURL provide admin URL configuration for Keycloak
|
||||
func WithAdminURL(url string) Option {
|
||||
return func(p *KeycloakProvider) {
|
||||
p.adminURL = url
|
||||
}
|
||||
}
|
||||
|
||||
// WithRealm provide realm configuration for Keycloak
|
||||
func WithRealm(realm string) Option {
|
||||
return func(p *KeycloakProvider) {
|
||||
p.realm = realm
|
||||
}
|
||||
}
|
||||
|
||||
// KeyCloak initializes a new keycloak provider
|
||||
func KeyCloak(opts ...Option) (Provider, error) {
|
||||
p := &KeycloakProvider{}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(p)
|
||||
}
|
||||
|
||||
if p.adminURL == "" {
|
||||
return nil, errors.New("Admin URL cannot be empty")
|
||||
}
|
||||
|
||||
_, err := url.Parse(p.adminURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to parse the adminURL %s: %w", p.adminURL, err)
|
||||
}
|
||||
|
||||
if p.client.Transport == nil {
|
||||
p.client.Transport = http.DefaultTransport
|
||||
}
|
||||
|
||||
if p.oeConfig.TokenEndpoint == "" {
|
||||
return nil, errors.New("missing OpenID token endpoint")
|
||||
}
|
||||
|
||||
if p.realm == "" {
|
||||
p.realm = "master" // default realm
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
59
internal/config/identity/openid/provider/provider.go
Normal file
59
internal/config/identity/openid/provider/provider.go
Normal file
@ -0,0 +1,59 @@
|
||||
// Copyright (c) 2015-2021 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 provider
|
||||
|
||||
import "errors"
|
||||
|
||||
// DiscoveryDoc - parses the output from openid-configuration
|
||||
// for example https://accounts.google.com/.well-known/openid-configuration
|
||||
type DiscoveryDoc struct {
|
||||
Issuer string `json:"issuer,omitempty"`
|
||||
AuthEndpoint string `json:"authorization_endpoint,omitempty"`
|
||||
TokenEndpoint string `json:"token_endpoint,omitempty"`
|
||||
UserInfoEndpoint string `json:"userinfo_endpoint,omitempty"`
|
||||
RevocationEndpoint string `json:"revocation_endpoint,omitempty"`
|
||||
JwksURI string `json:"jwks_uri,omitempty"`
|
||||
ResponseTypesSupported []string `json:"response_types_supported,omitempty"`
|
||||
SubjectTypesSupported []string `json:"subject_types_supported,omitempty"`
|
||||
IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported,omitempty"`
|
||||
ScopesSupported []string `json:"scopes_supported,omitempty"`
|
||||
TokenEndpointAuthMethods []string `json:"token_endpoint_auth_methods_supported,omitempty"`
|
||||
ClaimsSupported []string `json:"claims_supported,omitempty"`
|
||||
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported,omitempty"`
|
||||
}
|
||||
|
||||
// User represents information about user.
|
||||
type User struct {
|
||||
Name string `json:"username"`
|
||||
ID string `json:"id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
// Standard errors.
|
||||
var (
|
||||
ErrNotImplemented = errors.New("function not implemented")
|
||||
ErrAccessTokenExpired = errors.New("access_token expired or unauthorized")
|
||||
)
|
||||
|
||||
// Provider implements indentity provider specific admin operations, such as
|
||||
// looking up users, fetching additional attributes etc.
|
||||
type Provider interface {
|
||||
LoginWithUser(username, password string) error
|
||||
LoginWithClientID(clientID, clientSecret string) error
|
||||
LookupUser(userid string) (User, error)
|
||||
}
|
Loading…
Reference in New Issue
Block a user