feature: auto create domain for sso idp

This commit is contained in:
Qiu Jian
2020-10-29 00:47:48 +08:00
parent c2675d1547
commit 178e1ab8fc
11 changed files with 175 additions and 100 deletions
+23 -1
View File
@@ -265,9 +265,12 @@ func init() {
type IdentityProviderCreateCASOptions struct {
NAME string `help:"name of identity provider" json:"-"`
AutoCreateProject bool `help:"automatically create a default project when importing domain" json:"-"`
AutoCreateProject bool `help:"automatically create a project if the default_project not exists" json:"-"`
NoAutoCreateProject bool `help:"do not create default project when importing domain" json:"-"`
AutoCreateUser bool `help:"automatically create a user" json:"-"`
NoAutoCreateUser bool `help:"do not automatically create a user" json:"-"`
TargetDomain string `help:"target domain without creating new domain" json:"-"`
api.SCASIdpConfigOptions
@@ -284,6 +287,11 @@ func init() {
} else if args.NoAutoCreateProject {
params.Add(jsonutils.JSONFalse, "auto_create_project")
}
if args.AutoCreateUser {
params.Add(jsonutils.JSONTrue, "auto_create_user")
} else if args.NoAutoCreateUser {
params.Add(jsonutils.JSONFalse, "auto_create_user")
}
params.Add(jsonutils.NewString("cas"), "driver")
params.Add(jsonutils.Marshal(args), "config", "cas")
@@ -706,4 +714,18 @@ func init() {
printObject(result)
return nil
})
type IdpSetDefaultSsoOptions struct {
ID string `help:"id or name of idp to set default Sso" json:"-"`
api.PerformDefaultSsoInput
}
R(&IdpSetDefaultSsoOptions{}, "idp-default-sso", "Enable/disable default SSO", func(s *mcclient.ClientSession, args *IdpSetDefaultSsoOptions) error {
result, err := modules.IdentityProviders.PerformAction(s, args.ID, "default-sso", jsonutils.Marshal(args))
if err != nil {
return err
}
printObject(result)
return nil
})
}
+1 -1
View File
@@ -175,7 +175,7 @@ func (h *AuthHandlers) GetRegionsResponse(ctx context.Context, w http.ResponseWr
}
retIdps := make([]jsonutils.JSONObject, 0)
for i := range idps.Data {
retIdp := idps.Data[i].(*jsonutils.JSONDict).CopyIncludes("id", "name", "driver", "template", "icon_uri")
retIdp := idps.Data[i].(*jsonutils.JSONDict).CopyIncludes("id", "name", "driver", "template", "icon_uri", "is_default")
retIdps = append(retIdps, retIdp)
}
+4
View File
@@ -122,3 +122,7 @@ type GetIdpSsoRedirectUriOutput struct {
// Driver
Driver string `json:"driver"`
}
type PerformDefaultSsoInput struct {
Enable *bool `json:"enable" help:"enable default sso" negative:"disable"`
}
+3
View File
@@ -15,6 +15,9 @@
package identity
type SIdpAttributeOptions struct {
DomainNameAttribute string `json:"domain_name_attribute"`
DomainIdAttribute string `json:"domain_id_attribute"`
UserNameAttribute string `json:"user_name_attribute"`
UserIdAttribute string `json:"user_id_attribute"`
+2
View File
@@ -140,6 +140,8 @@ type SIdentityProvider struct {
IconUri string `json:"icon_uri"`
// 是否是SSO登录方式
IsSso *bool `json:"is_sso,omitempty"`
// 是否是缺省SSO登录方式
IsDefault *bool `json:"is_default,omitempty"`
}
// SIdmapping is an autogenerated struct via yunion.io/x/onecloud/pkg/keystone/models.SIdmapping.
+8 -22
View File
@@ -25,7 +25,6 @@ import (
"yunion.io/x/pkg/errors"
api "yunion.io/x/onecloud/pkg/apis/identity"
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/keystone/driver"
"yunion.io/x/onecloud/pkg/keystone/models"
"yunion.io/x/onecloud/pkg/mcclient"
@@ -135,41 +134,28 @@ func (self *SCASDriver) Authenticate(ctx context.Context, ident mcclient.SAuthen
log.Debugf("CAS response: %s qs: %s", resp, query.QueryString())
attrs := fetchAttributes(resp)
var usrId, usrName string
var domainId, domainName, usrId, usrName string
if v, ok := attrs[self.casConfig.DomainIdAttribute]; ok && len(v) > 0 {
domainId = v[0]
}
if v, ok := attrs[self.casConfig.DomainNameAttribute]; ok && len(v) > 0 {
domainName = v[0]
}
if v, ok := attrs[self.casConfig.UserIdAttribute]; ok && len(v) > 0 {
usrId = v[0]
}
if v, ok := attrs[self.casConfig.UserNameAttribute]; ok && len(v) > 0 {
usrName = v[0]
}
if len(usrId) == 0 && len(usrName) == 0 {
return nil, errors.Wrap(httperrors.ErrUnauthenticated, "empty userId or userName")
}
if len(usrId) == 0 {
usrId = usrName
} else if len(usrName) == 0 {
usrName = usrId
}
if len(usrId) == 0 {
return nil, errors.Wrap(httperrors.ErrUnauthenticated, "empty cas:user")
}
idp, err := models.IdentityProviderManager.FetchIdentityProviderById(self.IdpId)
if err != nil {
return nil, errors.Wrap(err, "self.GetIdentityProvider")
}
domain, usr, err := idp.SyncOrCreateDomainAndUser(ctx, usrId, usrName)
domain, usr, err := idp.SyncOrCreateDomainAndUser(ctx, domainId, domainName, usrId, usrName)
if err != nil {
return nil, errors.Wrap(err, "idp.SyncOrCreateDomainAndUser")
}
/*domain, err := idp.GetSingleDomain(ctx, api.DefaultRemoteDomainId, self.IdpName, fmt.Sprintf("cas provider %s", self.IdpName), false)
if err != nil {
return nil, errors.Wrap(err, "idp.GetSingleDomain")
}
usr, err := idp.SyncOrCreateUser(ctx, usrId, usrName, domain.Id, true, nil)
if err != nil {
return nil, errors.Wrap(err, "idp.SyncOrCreateUser")
}*/
extUser, err := models.UserManager.FetchUserExtended(usr.Id, "", "", "")
if err != nil {
return nil, errors.Wrap(err, "models.UserManager.FetchUserExtended")
+8 -18
View File
@@ -87,38 +87,28 @@ func (self *SOAuth2Driver) Authenticate(ctx context.Context, ident mcclient.SAut
return nil, errors.Wrapf(err, "driver %s Authenticate", self.Template)
}
var usrId, usrName string
var domainId, domainName, usrId, usrName string
if v, ok := attrs[options.DomainIdAttribute]; ok && len(v) > 0 {
domainId = v[0]
}
if v, ok := attrs[options.DomainNameAttribute]; ok && len(v) > 0 {
domainName = v[0]
}
if v, ok := attrs[options.UserIdAttribute]; ok && len(v) > 0 {
usrId = v[0]
}
if v, ok := attrs[options.UserNameAttribute]; ok && len(v) > 0 {
usrName = v[0]
}
if len(usrId) == 0 && len(usrName) == 0 {
return nil, errors.Wrap(httperrors.ErrUnauthenticated, "empty userId or userName")
}
if len(usrId) == 0 {
usrId = usrName
} else if len(usrName) == 0 {
usrName = usrId
}
idp, err := models.IdentityProviderManager.FetchIdentityProviderById(self.IdpId)
if err != nil {
return nil, errors.Wrap(err, "self.GetIdentityProvider")
}
domain, usr, err := idp.SyncOrCreateDomainAndUser(ctx, usrId, usrName)
domain, usr, err := idp.SyncOrCreateDomainAndUser(ctx, domainId, domainName, usrId, usrName)
if err != nil {
return nil, errors.Wrap(err, "idp.SyncOrCreateDomainAndUser")
}
/*domain, err := idp.GetSingleDomain(ctx, api.DefaultRemoteDomainId, self.IdpName, fmt.Sprintf("OpenID Connect/OAuth2.0 provider %s", self.IdpName), false)
if err != nil {
return nil, errors.Wrap(err, "idp.GetSingleDomain")
}
usr, err := idp.SyncOrCreateUser(ctx, usrId, usrName, domain.Id, true, nil)
if err != nil {
return nil, errors.Wrap(err, "idp.SyncOrCreateUser")
}*/
extUser, err := models.UserManager.FetchUserExtended(usr.Id, "", "", "")
if err != nil {
return nil, errors.Wrap(err, "models.UserManager.FetchUserExtended")
+8 -19
View File
@@ -24,7 +24,6 @@ import (
"yunion.io/x/pkg/errors"
api "yunion.io/x/onecloud/pkg/apis/identity"
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/keystone/driver"
"yunion.io/x/onecloud/pkg/keystone/models"
"yunion.io/x/onecloud/pkg/mcclient"
@@ -150,38 +149,28 @@ func (self *SOIDCDriver) Authenticate(ctx context.Context, ident mcclient.SAuthe
attrs[k] = []string{v}
}
var usrId, usrName string
var domainId, domainName, usrId, usrName string
if v, ok := attrs[self.oidcConfig.DomainIdAttribute]; ok && len(v) > 0 {
domainId = v[0]
}
if v, ok := attrs[self.oidcConfig.DomainNameAttribute]; ok && len(v) > 0 {
domainName = v[0]
}
if v, ok := attrs[self.oidcConfig.UserIdAttribute]; ok && len(v) > 0 {
usrId = v[0]
}
if v, ok := attrs[self.oidcConfig.UserNameAttribute]; ok && len(v) > 0 {
usrName = v[0]
}
if len(usrId) == 0 && len(usrName) == 0 {
return nil, errors.Wrap(httperrors.ErrUnauthenticated, "empty userId or userName")
}
if len(usrId) == 0 {
usrId = usrName
} else if len(usrName) == 0 {
usrName = usrId
}
idp, err := models.IdentityProviderManager.FetchIdentityProviderById(self.IdpId)
if err != nil {
return nil, errors.Wrap(err, "self.GetIdentityProvider")
}
domain, usr, err := idp.SyncOrCreateDomainAndUser(ctx, usrId, usrName)
domain, usr, err := idp.SyncOrCreateDomainAndUser(ctx, domainId, domainName, usrId, usrName)
if err != nil {
return nil, errors.Wrap(err, "idp.SyncOrCreateDomainAndUser")
}
/*domain, err := idp.GetSingleDomain(ctx, api.DefaultRemoteDomainId, self.IdpName, fmt.Sprintf("OpenID Connect/OAuth2.0 provider %s", self.IdpName), false)
if err != nil {
return nil, errors.Wrap(err, "idp.GetSingleDomain")
}
usr, err := idp.SyncOrCreateUser(ctx, usrId, usrName, domain.Id, true, nil)
if err != nil {
return nil, errors.Wrap(err, "idp.SyncOrCreateUser")
}*/
extUser, err := models.UserManager.FetchUserExtended(usr.Id, "", "", "")
if err != nil {
return nil, errors.Wrap(err, "models.UserManager.FetchUserExtended")
+8 -33
View File
@@ -119,54 +119,29 @@ func (self *SSAMLDriver) Authenticate(ctx context.Context, ident mcclient.SAuthe
attrs := resp.FetchAttribtues()
var usrId, usrName string
var domainId, domainName, usrId, usrName string
if v, ok := attrs[self.samlConfig.DomainIdAttribute]; ok && len(v) > 0 {
domainId = v[0]
}
if v, ok := attrs[self.samlConfig.DomainNameAttribute]; ok && len(v) > 0 {
domainName = v[0]
}
if v, ok := attrs[self.samlConfig.UserIdAttribute]; ok && len(v) > 0 {
usrId = v[0]
}
if v, ok := attrs[self.samlConfig.UserNameAttribute]; ok && len(v) > 0 {
usrName = v[0]
}
if len(usrId) == 0 && len(usrName) == 0 {
return nil, errors.Wrap(httperrors.ErrUnauthenticated, "empty userId or userName")
}
if len(usrId) == 0 {
usrId = usrName
} else if len(usrName) == 0 {
usrName = usrId
}
idp, err := models.IdentityProviderManager.FetchIdentityProviderById(self.IdpId)
if err != nil {
return nil, errors.Wrap(err, "self.GetIdentityProvider")
}
domain, usr, err := idp.SyncOrCreateDomainAndUser(ctx, usrId, usrName)
domain, usr, err := idp.SyncOrCreateDomainAndUser(ctx, domainId, domainName, usrId, usrName)
if err != nil {
return nil, errors.Wrap(err, "idp.SyncOrCreateDomainAndUser")
}
/*if idp.AutoCreateUser.IsTrue() {
domain, err = idp.GetSingleDomain(ctx, api.DefaultRemoteDomainId, self.IdpName, fmt.Sprintf("SAML 2.0 provider %s", self.IdpName), false)
if err != nil {
return nil, errors.Wrap(err, "idp.GetSingleDomain")
}
usr, err = idp.SyncOrCreateUser(ctx, usrId, usrName, domain.Id, true, nil)
if err != nil {
return nil, errors.Wrap(err, "idp.SyncOrCreateUser")
}
} else {
modelUsrId, err := models.IdmappingManager.FetchByIdpAndEntityId(ctx, idp.Id, usrId, api.IdMappingEntityUser)
if err != nil {
if errors.Cause(err) == sql.ErrNoRows {
return nil, errors.Wrap(httperrors.ErrUserNotFound, usrId)
}
}
usrObj, err := models.UserManager.FetchById(modelUsrId)
if err != nil {
return nil, errors.Wrap(err, "UserManager.FetchById")
}
usr = usrObj.(*models.SUser)
domain = usr.GetDomain()
}*/
extUser, err := models.UserManager.FetchUserExtended(usr.Id, "", "", "")
if err != nil {
return nil, errors.Wrap(err, "models.UserManager.FetchUserExtended")
+2 -2
View File
@@ -65,7 +65,7 @@ type SIdmapping struct {
PublicId string `width:"64" charset:"ascii" nullable:"false" primary:"false"`
IdpId string `name:"domain_id" width:"64" charset:"ascii" nullable:"false" primary:"true"`
IdpEntityId string `name:"local_id" width:"128" charset:"utf8" nullable:"false" primary:"true"`
EntityType string `width:"10" charset:"ascii" nullable:"false"`
EntityType string `width:"10" charset:"ascii" nullable:"false" primary:"true"`
}
func getIdmapKey(idpId string, entityId string, entityType string) string {
@@ -104,7 +104,7 @@ func (manager *SIdmappingManager) RegisterIdMapWithId(ctx context.Context, idpId
mapping.IdpEntityId = entityId
mapping.EntityType = entityType
err = manager.TableSpec().InsertOrUpdate(ctx, &mapping)
err = manager.TableSpec().Insert(ctx, &mapping)
if err != nil {
return "", errors.Wrap(err, "Insert")
}
+108 -4
View File
@@ -104,6 +104,8 @@ type SIdentityProvider struct {
IconUri string `width:"256" charset:"utf8" nullable:"true" list:"user" create:"domain_optional" update:"domain"`
// 是否是SSO登录方式
IsSso tristate.TriState `nullable:"true" list:"domain"`
// 是否是缺省SSO登录方式
IsDefault tristate.TriState `nullable:"true" list:"domain"`
}
func (manager *SIdentityProviderManager) initializeAutoCreateUser() error {
@@ -498,7 +500,7 @@ func (ident *SIdentityProvider) PostCreate(ctx context.Context, userCred mcclien
return
}
if len(ident.TargetDomainId) == 0 && ident.AutoCreateUser.IsTrue() && ident.IsSso.IsTrue() {
if len(ident.TargetDomainId) == 0 && ident.AutoCreateUser.IsTrue() && ident.IsSso.IsTrue() && !ident.isAutoCreateDomain() {
// SSO driver need to create the target domain immediately
domain, err := ident.SyncOrCreateDomain(ctx, api.DefaultRemoteDomainId, ident.Name, fmt.Sprintf("%s provider %s", ident.Driver, ident.Name), false)
if err != nil {
@@ -793,7 +795,7 @@ func (self *SIdentityProvider) ValidateDeleteCondition(ctx context.Context) erro
if self.Enabled.IsTrue() {
return httperrors.NewInvalidStatusError("cannot delete enabled idp")
}
if self.Driver == api.IdentityDriverLDAP || self.AutoCreateUser.IsTrue() {
if self.Driver == api.IdentityDriverLDAP || (self.IsSso.IsTrue() && self.isAutoCreateDomain()) || self.AutoCreateUser.IsTrue() {
prjCnt, err := self.GetProjectCount()
if err != nil {
return httperrors.NewGeneralError(err)
@@ -980,6 +982,7 @@ func (self *SIdentityProvider) GetSingleDomain(ctx context.Context, extId string
}
func (self *SIdentityProvider) SyncOrCreateDomain(ctx context.Context, extId string, extName string, extDesc string, createDefaultProject bool) (*SDomain, error) {
log.Debugf("SyncOrCreateDomain extId: %s extName: %s", extId, extName)
domainId, err := IdmappingManager.RegisterIdMap(ctx, self.Id, extId, api.IdMappingEntityDomain)
if err != nil {
return nil, errors.Wrap(err, "IdmappingManager.RegisterIdMap")
@@ -989,6 +992,7 @@ func (self *SIdentityProvider) SyncOrCreateDomain(ctx context.Context, extId str
return nil, errors.Wrap(err, "DomainManager.FetchDomainById")
}
if err == nil {
// find the domain
if domain.Name != extName {
// sync domain name
newName, err := db.GenerateName2(DomainManager, nil, extName, domain, 1)
@@ -1007,6 +1011,7 @@ func (self *SIdentityProvider) SyncOrCreateDomain(ctx context.Context, extId str
return domain, nil
}
// otherwise, create the domain
lockman.LockClass(ctx, DomainManager, "")
defer lockman.ReleaseClass(ctx, DomainManager, "")
@@ -1358,14 +1363,38 @@ func (idp *SIdentityProvider) GetDetailsSsoRedirectUri(ctx context.Context, user
return output, nil
}
func (idp *SIdentityProvider) SyncOrCreateDomainAndUser(ctx context.Context, extUsrId, extUsrName string) (*SDomain, *SUser, error) {
func (idp *SIdentityProvider) SyncOrCreateDomainAndUser(ctx context.Context, extDomainId, extDomainName string, extUsrId, extUsrName string) (*SDomain, *SUser, error) {
var (
domain *SDomain
usr *SUser
err error
)
if len(extUsrId) == 0 && len(extUsrName) == 0 {
return nil, nil, errors.Wrap(httperrors.ErrUnauthenticated, "empty userId or userName")
}
if len(extUsrId) == 0 {
extUsrId = extUsrName
} else if len(extUsrName) == 0 {
extUsrName = extUsrId
}
var domainDesc string
if len(extDomainId) == 0 && len(extDomainName) == 0 {
extDomainId = api.DefaultRemoteDomainId
extDomainName = idp.Name
domainDesc = fmt.Sprintf("%s provider %s", idp.Driver, idp.Name)
} else if len(extDomainId) == 0 {
extDomainId = extDomainName
domainDesc = fmt.Sprintf("%s provider %s autocreated for %s", idp.Driver, idp.Name, extDomainName)
} else if len(extDomainName) == 0 {
extDomainName = extDomainId
domainDesc = fmt.Sprintf("%s provider %s autocreated for %s", idp.Driver, idp.Name, extDomainId)
} else {
domainDesc = fmt.Sprintf("%s provider %s autocreated for %s(%s)", idp.Driver, idp.Name, extDomainName, extDomainId)
}
if idp.AutoCreateUser.IsTrue() {
domain, err = idp.GetSingleDomain(ctx, api.DefaultRemoteDomainId, idp.Name, fmt.Sprintf("%s provider %s", idp.Driver, idp.Name), false)
domain, err = idp.GetSingleDomain(ctx, extDomainId, extDomainName, domainDesc, false)
if err != nil {
return nil, nil, errors.Wrap(err, "idp.GetSingleDomain")
}
@@ -1481,3 +1510,78 @@ func (idp *SIdentityProvider) PerformEnable(
}
return idp.SEnabledStatusStandaloneResourceBase.PerformEnable(ctx, userCred, query, input)
}
func (idp *SIdentityProvider) isAutoCreateDomain() bool {
configs, err := GetConfigs(idp, false, nil, nil)
if err != nil {
log.Errorf("GetConfigs fail %s", err)
return false
}
if vjson, ok := configs[idp.Driver]["domain_id_attribute"]; ok {
v, _ := vjson.GetString()
if len(v) > 0 {
return true
}
}
if vjson, ok := configs[idp.Driver]["domain_id_attribute"]; ok {
v, _ := vjson.GetString()
if len(v) > 0 {
return true
}
}
return false
}
func (idp *SIdentityProvider) PerformDefaultSso(
ctx context.Context,
userCred mcclient.TokenCredential,
query jsonutils.JSONObject,
input api.PerformDefaultSsoInput,
) (jsonutils.JSONObject, error) {
if !idp.IsSso.IsTrue() {
return nil, errors.Wrap(httperrors.ErrNotSupported, "idp is not a sso idp")
}
if input.Enable != nil {
if *input.Enable {
// enable
// first disable any other idp
q := IdentityProviderManager.Query().IsTrue("is_sso").IsTrue("is_default").NotEquals("id", idp.Id)
idps := make([]SIdentityProvider, 0)
err := db.FetchModelObjects(IdentityProviderManager, q, &idps)
if err != nil && errors.Cause(err) != sql.ErrNoRows {
return nil, errors.Wrap(err, "FetchModelObjects")
}
for i := range idps {
err := idps[i].setIsDefault(tristate.False)
if err != nil {
return nil, errors.Wrap(err, "disable other idp fail")
}
}
if !idp.IsDefault.IsTrue() {
err := idp.setIsDefault(tristate.True)
if err != nil {
return nil, errors.Wrap(err, "update is_default fail")
}
}
} else {
// disable
if idp.IsDefault.IsTrue() {
err := idp.setIsDefault(tristate.False)
if err != nil {
return nil, errors.Wrap(err, "update is_default fail")
}
}
}
}
return nil, nil
}
func (idp *SIdentityProvider) setIsDefault(val tristate.TriState) error {
_, err := db.Update(idp, func() error {
idp.IsDefault = val
return nil
})
return errors.Wrap(err, "update")
}