Merge pull request #5796 from swordqiu/feature/qj-cas-user-join-project-3.1

feature: cas user auto join designated project
This commit is contained in:
Zexi Li
2020-04-14 16:28:40 +08:00
committed by GitHub
15 changed files with 291 additions and 61 deletions
+12
View File
@@ -14,8 +14,20 @@
package identity
import "yunion.io/x/pkg/tristate"
type SCASIdpConfigOptions struct {
// https://cas.example.org/cas/
CASServerURL string `json:"cas_server_url"`
Service string `json:"service"`
CasProjectAttribute string `json:"cas_project_attribute"`
AutoCreateCasProject tristate.TriState `json:"auto_create_cas_project"`
DefaultCasProjectId string `json:"default_cas_project_id"`
CasRoleAttribute string `json:"cas_role_attribute"`
DefaultCasRoleId string `json:"default_cas_role_id"`
}
+16
View File
@@ -93,3 +93,19 @@ var (
IdpTemplateOpenLDAPSingleDomain: IdentityDriverLDAP,
}
)
type PerformConfigInput struct {
// 更新配置的方式
// example: update
//
// | action | 含义 |
// |---------|-----------------------------------------------|
// | update | 增量更新配置 |
// | remove | 删除指定配置 |
// | replace | 全量替换配置,如果action为空,则默认为replace |
//
Action string `json:"action"`
// 配置信息
Config TConfigs `json:"config"`
}
+25
View File
@@ -29,3 +29,28 @@ type IdentityProviderDetails struct {
ProjectCount int `json:"project_count,allowempty"`
GroupCount int `json:"group_count,allowempty"`
}
type IdentityProviderCreateInput struct {
apis.EnabledStatusStandaloneResourceCreateInput
// 后端驱动名称
Driver string `json:"driver"`
// 模板名称
Template string `json:"template"`
// 默认导入用户和组的域
TargetDomainId string `json:"target_domain_id"`
// swagger:ignore
// Deprecated
TargetDomain string `json:"target_domain" "yunion:deprecated-by":"target_domain_id"`
// 新建域的时候是否自动新建第一个项目
AutoCreateProject *bool `json:"auto_create_project"`
// 自动同步间隔,单位:秒
SyncIntervalSeconds *int `json:"sync_interval_seconds"`
// 配置信息
Config TConfigs `json:"config"`
}
+66
View File
@@ -16,8 +16,11 @@ package cas
import (
"context"
"database/sql"
"encoding/xml"
"fmt"
"regexp"
"strings"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
@@ -144,9 +147,72 @@ func (self *SCASDriver) Authenticate(ctx context.Context, ident mcclient.SAuthen
if err != nil {
return nil, errors.Wrap(err, "models.UserManager.FetchUserExtended")
}
self.userTryJoinProject(ctx, usr, domain, resp)
return extUser, nil
}
func (self *SCASDriver) userTryJoinProject(ctx context.Context, usr *models.SUser, domain *models.SDomain, resp []byte) {
var err error
var targetProject *models.SProject
if len(self.casConfig.CasProjectAttribute) > 0 {
projName := fetchAttribute(resp, self.casConfig.CasProjectAttribute)
if len(projName) > 0 {
targetProject, err = models.ProjectManager.FetchProject("", projName, domain.Id, domain.Name)
if err != nil {
log.Errorf("fetch project %s fail %s", projName, err)
if errors.Cause(err) == sql.ErrNoRows && self.casConfig.AutoCreateCasProject.IsTrue() {
targetProject, err = models.ProjectManager.NewProject(ctx, projName, "cas project", domain)
if err != nil {
log.Errorf("auto create project %s fail %s", projName, err)
}
}
}
}
}
if targetProject == nil && len(self.casConfig.DefaultCasProjectId) > 0 {
targetProject, err = models.ProjectManager.FetchProjectById(self.casConfig.DefaultCasProjectId)
if err != nil {
log.Errorf("fetch default project %s fail %s", self.casConfig.DefaultCasProjectId, err)
}
}
if targetProject != nil {
// put user in project
var targetRole *models.SRole
if len(self.casConfig.CasRoleAttribute) > 0 {
roleName := fetchAttribute(resp, self.casConfig.CasRoleAttribute)
if len(roleName) > 0 {
targetRole, err = models.RoleManager.FetchRole("", roleName, domain.Id, domain.Name)
if err != nil {
log.Errorf("fetch role %s fail %s", roleName, err)
}
}
}
if targetRole == nil && len(self.casConfig.DefaultCasRoleId) > 0 {
targetRole, err = models.RoleManager.FetchRoleById(self.casConfig.DefaultCasRoleId)
if err != nil {
log.Errorf("fetch default role %s fail %s", self.casConfig.DefaultCasRoleId, err)
}
}
if targetRole != nil {
err = models.AssignmentManager.ProjectAddUser(ctx, models.GetDefaultAdminCred(), targetProject, usr, targetRole)
if err != nil {
log.Errorf("CAS user join project fail %s", err)
}
}
}
}
func fetchAttribute(heystack []byte, name string) string {
pattern := regexp.MustCompile(fmt.Sprintf(`<%s>(\w+)</%s>`, name, name))
result := pattern.FindAllStringSubmatch(string(heystack), -1)
if len(result) > 0 && len(result[0]) > 1 {
return strings.TrimSpace(result[0][1])
}
return ""
}
func (self *SCASDriver) Sync(ctx context.Context) error {
return nil
}
+14
View File
@@ -46,3 +46,17 @@ func TestXmlUnmarshal(t *testing.T) {
t.Logf("%#v", casresp)
}
}
func TestFetchAttribute(t *testing.T) {
xmlstr := `<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
<cas:authenticationSuccess>
<cas:user>casuser</cas:user>
<cas:proj>casproj</cas:proj>
</cas:authenticationSuccess>
</cas:serviceResponse>`
got := fetchAttribute([]byte(xmlstr), "cas:proj")
want := "casproj"
if got != want {
t.Errorf("want %s got %s", want, got)
}
}
+43
View File
@@ -15,8 +15,17 @@
package cas
import (
"context"
"database/sql"
"yunion.io/x/jsonutils"
"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"
)
type SCASDriverClass struct{}
@@ -37,6 +46,40 @@ func (self *SCASDriverClass) Name() string {
return api.IdentityDriverCAS
}
func (self *SCASDriverClass) ValidateConfig(ctx context.Context, userCred mcclient.TokenCredential, tconf api.TConfigs) (api.TConfigs, error) {
conf := api.SCASIdpConfigOptions{}
confJson := jsonutils.Marshal(tconf["cas"])
err := confJson.Unmarshal(&conf)
if err != nil {
return tconf, errors.Wrap(err, "unmarshal config")
}
if len(conf.DefaultCasProjectId) > 0 {
obj, err := models.ProjectManager.FetchByIdOrName(userCred, conf.DefaultCasProjectId)
if err != nil {
if errors.Cause(err) == sql.ErrNoRows {
return tconf, errors.Wrapf(httperrors.ErrResourceNotFound, "project %s", conf.DefaultCasProjectId)
} else {
return tconf, errors.Wrap(err, "FetchProjectById")
}
}
tconf["cas"]["default_cas_project_id"] = jsonutils.NewString(obj.GetId())
}
if len(conf.DefaultCasRoleId) > 0 {
obj, err := models.RoleManager.FetchByIdOrName(userCred, conf.DefaultCasRoleId)
if err != nil {
if errors.Cause(err) == sql.ErrNoRows {
return tconf, errors.Wrapf(httperrors.ErrResourceNotFound, "role %s", conf.DefaultCasRoleId)
} else {
return tconf, errors.Wrap(err, "FetchRoleById")
}
}
tconf["cas"]["default_cas_role_id"] = jsonutils.NewString(obj.GetId())
}
return tconf, nil
}
func init() {
driver.RegisterDriverClass(&SCASDriverClass{})
}
+1
View File
@@ -26,6 +26,7 @@ type IIdentityBackendClass interface {
SyncMethod() string
Name() string
NewDriver(idpId, idpName, template, targetDomainId string, autoCreateProject bool, conf api.TConfigs) (IIdentityBackend, error)
ValidateConfig(ctx context.Context, userCred mcclient.TokenCredential, conf api.TConfigs) (api.TConfigs, error)
}
type IIdentityBackend interface {
+7
View File
@@ -15,8 +15,11 @@
package ldap
import (
"context"
api "yunion.io/x/onecloud/pkg/apis/identity"
"yunion.io/x/onecloud/pkg/keystone/driver"
"yunion.io/x/onecloud/pkg/mcclient"
)
type SLDAPDriverClass struct{}
@@ -37,6 +40,10 @@ func (self *SLDAPDriverClass) Name() string {
return api.IdentityDriverLDAP
}
func (self *SLDAPDriverClass) ValidateConfig(ctx context.Context, userCred mcclient.TokenCredential, conf api.TConfigs) (api.TConfigs, error) {
return conf, nil
}
func init() {
driver.RegisterDriverClass(&SLDAPDriverClass{})
}
+7
View File
@@ -15,8 +15,11 @@
package sql
import (
"context"
api "yunion.io/x/onecloud/pkg/apis/identity"
"yunion.io/x/onecloud/pkg/keystone/driver"
"yunion.io/x/onecloud/pkg/mcclient"
)
type SSQLDriverClass struct{}
@@ -37,6 +40,10 @@ func (self *SSQLDriverClass) Name() string {
return api.IdentityDriverSQL
}
func (self *SSQLDriverClass) ValidateConfig(ctx context.Context, userCred mcclient.TokenCredential, conf api.TConfigs) (api.TConfigs, error) {
return conf, nil
}
func init() {
driver.RegisterDriverClass(&SSQLDriverClass{})
}
+1 -1
View File
@@ -242,7 +242,7 @@ func (manager *SAssignmentManager) fetchProjectUserIdsQuery(projId string) *sqlc
return union.Query()
}
func (manager *SAssignmentManager) projectAddUser(ctx context.Context, userCred mcclient.TokenCredential, project *SProject, user *SUser, role *SRole) error {
func (manager *SAssignmentManager) ProjectAddUser(ctx context.Context, userCred mcclient.TokenCredential, project *SProject, user *SUser, role *SRole) error {
err := db.ValidateCreateDomainId(project.DomainId)
if err != nil {
return err
+50 -51
View File
@@ -26,7 +26,6 @@ import (
"yunion.io/x/pkg/tristate"
"yunion.io/x/sqlchemy"
"yunion.io/x/onecloud/pkg/apis"
api "yunion.io/x/onecloud/pkg/apis/identity"
"yunion.io/x/onecloud/pkg/cloudcommon/consts"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
@@ -221,23 +220,30 @@ func (self *SIdentityProvider) GetDetailsConfig(ctx context.Context, userCred mc
return result, nil
}
func (ident *SIdentityProvider) AllowPerformConfig(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data *jsonutils.JSONDict) bool {
func (ident *SIdentityProvider) getDriverClass() driver.IIdentityBackendClass {
return driver.GetDriverClass(ident.Driver)
}
func (ident *SIdentityProvider) AllowPerformConfig(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.PerformConfigInput) bool {
return db.IsAdminAllowUpdateSpec(userCred, ident, "config")
}
func (ident *SIdentityProvider) PerformConfig(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data *jsonutils.JSONDict) (jsonutils.JSONObject, error) {
func (ident *SIdentityProvider) PerformConfig(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.PerformConfigInput) (jsonutils.JSONObject, error) {
if ident.Status == api.IdentityDriverStatusConnected && ident.Enabled {
return nil, httperrors.NewInvalidStatusError("cannot update config when enabled and connected")
}
if ident.SyncStatus != api.IdentitySyncStatusIdle {
return nil, httperrors.NewInvalidStatusError("cannot update config when not idle")
}
opts := api.TConfigs{}
err := data.Unmarshal(&opts, "config")
var err error
input.Config, err = ident.getDriverClass().ValidateConfig(ctx, userCred, input.Config)
if err != nil {
return nil, httperrors.NewInputParameterError("invalid input data")
return nil, errors.Wrap(err, "ValidateConfig")
}
action, _ := data.GetString("action")
opts := input.Config
action := input.Action
err = saveConfigs(action, ident, opts, nil, nil, api.SensitiveDomainConfigMap)
if err != nil {
return nil, httperrors.NewInternalServerError("saveConfig fail %s", err)
@@ -251,74 +257,77 @@ func (manager *SIdentityProviderManager) getDriveInstanceCount(drvName string) (
return manager.Query().Equals("driver", drvName).CountWithError()
}
func (manager *SIdentityProviderManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data *jsonutils.JSONDict) (*jsonutils.JSONDict, error) {
func (manager *SIdentityProviderManager) ValidateCreateData(
ctx context.Context,
userCred mcclient.TokenCredential,
ownerId mcclient.IIdentityProvider,
query jsonutils.JSONObject,
input api.IdentityProviderCreateInput,
) (api.IdentityProviderCreateInput, error) {
var drvName string
template, _ := data.GetString("template")
template := input.Template
if len(template) > 0 {
if _, ok := api.IdpTemplateDriver[template]; !ok {
return nil, httperrors.NewInputParameterError("invalid template")
return input, httperrors.NewInputParameterError("invalid template")
}
drvName = api.IdpTemplateDriver[template]
data.Set("driver", jsonutils.NewString(drvName))
input.Driver = drvName
} else {
drvName, _ = data.GetString("driver")
drvName = input.Driver
if len(drvName) == 0 {
return nil, httperrors.NewInputParameterError("missing driver")
return input, httperrors.NewInputParameterError("missing driver")
}
}
drvCls := driver.GetDriverClass(drvName)
if drvCls == nil {
return nil, httperrors.NewInputParameterError("driver %s not supported", drvName)
return input, httperrors.NewInputParameterError("driver %s not supported", drvName)
}
if drvCls.SingletonInstance() {
cnt, err := manager.getDriveInstanceCount(drvName)
if err != nil {
return nil, httperrors.NewGeneralError(err)
return input, httperrors.NewGeneralError(err)
}
if cnt >= 1 {
return nil, httperrors.NewConflictError("driver %s already exists", drvName)
return input, httperrors.NewConflictError("driver %s already exists", drvName)
}
}
if data.Contains("sync_interval_seconds") {
secs, _ := data.Int("sync_interval_seconds")
if input.SyncIntervalSeconds != nil {
secs := *input.SyncIntervalSeconds
if secs < api.MinimalSyncIntervalSeconds {
data.Set("sync_interval_seconds", jsonutils.NewInt(int64(api.MinimalSyncIntervalSeconds)))
secs = api.MinimalSyncIntervalSeconds
input.SyncIntervalSeconds = &secs
}
}
targetDomainStr, _ := data.GetString("target_domain")
targetDomainStr := input.TargetDomainId
if len(targetDomainStr) > 0 {
domain, err := DomainManager.FetchDomainById(targetDomainStr)
if err != nil {
if err == sql.ErrNoRows {
return nil, httperrors.NewResourceNotFoundError2(DomainManager.Keyword(), targetDomainStr)
return input, httperrors.NewResourceNotFoundError2(DomainManager.Keyword(), targetDomainStr)
} else {
return nil, httperrors.NewGeneralError(err)
return input, httperrors.NewGeneralError(err)
}
}
data.Set("target_domain_id", jsonutils.NewString(domain.Id))
input.TargetDomainId = domain.Id
}
opts := api.TConfigs{}
err := data.Unmarshal(&opts, "config")
var err error
input.Config, err = drvCls.ValidateConfig(ctx, userCred, input.Config)
if err != nil {
return nil, httperrors.NewInputParameterError("parse config error: %s", err)
return input, errors.Wrap(err, "ValidateConfig")
}
input := apis.EnabledStatusStandaloneResourceCreateInput{}
err = data.Unmarshal(&input)
input.EnabledStatusStandaloneResourceCreateInput, err = manager.SEnabledStatusStandaloneResourceBaseManager.ValidateCreateData(ctx, userCred, ownerId, query, input.EnabledStatusStandaloneResourceCreateInput)
if err != nil {
return nil, httperrors.NewInternalServerError("unmarshal StandaloneResourceCreateInput fail %s", err)
return input, errors.Wrap(err, "SEnabledStatusStandaloneResourceBaseManager.ValidateCreateData")
}
input, err = manager.SEnabledStatusStandaloneResourceBaseManager.ValidateCreateData(ctx, userCred, ownerId, query, input)
if err != nil {
return nil, err
}
data.Update(jsonutils.Marshal(input))
return data, nil
return input, nil
}
func (ident *SIdentityProvider) CustomizeCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) error {
@@ -736,23 +745,13 @@ func (self *SIdentityProvider) SyncOrCreateDomain(ctx context.Context, extId str
}
if self.AutoCreateProject.IsTrue() && consts.GetNonDefaultDomainProjects() {
project := &SProject{}
project.SetModelManager(ProjectManager, project)
projectName := NormalizeProjectName(fmt.Sprintf("%s_default_project", extName))
newName, err := db.GenerateName(ProjectManager, nil, projectName)
_, err := ProjectManager.NewProject(ctx,
fmt.Sprintf("%s_default_project", extName),
fmt.Sprintf("Default project for domain %s", extName),
domain,
)
if err != nil {
// ignore the error
log.Errorf("db.GenerateName error %s for default domain project %s", err, projectName)
newName = projectName
}
project.Name = newName
project.DomainId = domain.Id
project.Description = fmt.Sprintf("Default project for domain %s", extName)
project.IsDomain = tristate.False
project.ParentId = domain.Id
err = ProjectManager.TableSpec().Insert(project)
if err != nil {
log.Errorf("ProjectManager.Insert fail %s", err)
log.Errorf("ProjectManager.NewProject fail %s", err)
}
}
+46 -6
View File
@@ -29,10 +29,12 @@ import (
api "yunion.io/x/onecloud/pkg/apis/identity"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/cloudcommon/db/lockman"
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/keystone/options"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/util/pinyinutils"
"yunion.io/x/onecloud/pkg/util/rbacutils"
)
type SProjectManager struct {
@@ -321,12 +323,21 @@ func (proj *SProject) getExternalResources() (map[string]int, time.Time, error)
func NormalizeProjectName(name string) string {
name = pinyinutils.Text2Pinyin(name)
for _, illChar := range []string{
"/", ".", " ",
} {
name = strings.Replace(name, illChar, "", -1)
newName := strings.Builder{}
lastSlash := false
for _, c := range name {
if (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') {
newName.WriteRune(c)
lastSlash = false
} else if c >= 'A' && c <= 'Z' {
newName.WriteRune(c - 'A' + 'a')
lastSlash = false
} else if !lastSlash {
newName.WriteRune('-')
lastSlash = true
}
}
return name
return newName.String()
}
func (manager *SProjectManager) FetchUserProjects(userId string) ([]SProjectExtended, error) {
@@ -430,7 +441,7 @@ func (project *SProject) PerformJoin(
for i := range users {
for j := range roles {
err = AssignmentManager.projectAddUser(ctx, userCred, project, users[i], roles[j])
err = AssignmentManager.ProjectAddUser(ctx, userCred, project, users[i], roles[j])
if err != nil {
return nil, httperrors.NewGeneralError(err)
}
@@ -518,3 +529,32 @@ func (project *SProject) PerformLeave(
}
return nil, nil
}
func (manager *SProjectManager) NewProject(ctx context.Context, name string, desc string, domain *SDomain) (*SProject, error) {
lockman.LockClass(ctx, manager, domain.Id)
defer lockman.ReleaseClass(ctx, manager, domain.Id)
project := &SProject{}
project.SetModelManager(ProjectManager, project)
projectName := NormalizeProjectName(name)
ownerId := &db.SOwnerId{}
if manager.NamespaceScope() == rbacutils.ScopeDomain {
ownerId.DomainId = domain.Id
}
newName, err := db.GenerateName(ProjectManager, ownerId, projectName)
if err != nil {
// ignore the error
log.Errorf("db.GenerateName error %s for default domain project %s", err, projectName)
newName = projectName
}
project.Name = newName
project.DomainId = domain.Id
project.Description = desc
project.IsDomain = tristate.False
project.ParentId = domain.Id
err = ProjectManager.TableSpec().Insert(project)
if err != nil {
return nil, errors.Wrap(err, "Insert")
}
return project, nil
}
+1 -1
View File
@@ -24,7 +24,7 @@ func TestNormalizeProjectName(t *testing.T) {
Want string
}{
{"分公司1", "fengongsi1"},
{"集团/分公司/项目A", "jituanfengongsixiangmuA"},
{"集团/分公司/项目A", "jituan-fengongsi-xiangmua"},
}
for _, c := range cases {
got := NormalizeProjectName(c.In)
+1 -1
View File
@@ -278,7 +278,7 @@ func (role *SRole) UpdateInContext(ctx context.Context, userCred mcclient.TokenC
}
switch obj := ctxObjs[1].(type) {
case *SUser:
return nil, AssignmentManager.projectAddUser(ctx, userCred, project, obj, role)
return nil, AssignmentManager.ProjectAddUser(ctx, userCred, project, obj, role)
case *SGroup:
return nil, AssignmentManager.projectAddGroup(ctx, userCred, project, obj, role)
default:
+1 -1
View File
@@ -796,7 +796,7 @@ func joinProjects(ident db.IModel, isUser bool, ctx context.Context, userCred mc
for i := range projects {
for j := range roles {
if isUser {
err = AssignmentManager.projectAddUser(ctx, userCred, projects[i], ident.(*SUser), roles[j])
err = AssignmentManager.ProjectAddUser(ctx, userCred, projects[i], ident.(*SUser), roles[j])
} else {
err = AssignmentManager.projectAddGroup(ctx, userCred, projects[i], ident.(*SGroup), roles[j])
}