mirror of
https://github.com/yunionio/cloudpods.git
synced 2026-08-31 01:35:56 +08:00
feature: keystone support CAS SSO authentication
This commit is contained in:
@@ -114,17 +114,17 @@ func init() {
|
||||
return nil
|
||||
})
|
||||
|
||||
type IdentityProviderCreateOptions struct {
|
||||
type IdentityProviderCreateLDAPOptions struct {
|
||||
NAME string `help:"name of identity provider" json:"-"`
|
||||
|
||||
AutoCreateProject bool `help:"automatically create a default project when importing domain" json:"-"`
|
||||
NoAutoCreateProject bool `help:"do not create default project when importing domain" json:"-"`
|
||||
|
||||
TargetDomain string `help:"target domain without creating new domain"`
|
||||
TargetDomain string `help:"target domain without creating new domain" json:"-"`
|
||||
|
||||
api.SLDAPIdpConfigOptions
|
||||
}
|
||||
R(&IdentityProviderCreateOptions{}, "idp-create-ldap", "Create an identity provider with LDAP driver", func(s *mcclient.ClientSession, args *IdentityProviderCreateOptions) error {
|
||||
R(&IdentityProviderCreateLDAPOptions{}, "idp-create-ldap", "Create an identity provider with LDAP driver", func(s *mcclient.ClientSession, args *IdentityProviderCreateLDAPOptions) error {
|
||||
params := jsonutils.NewDict()
|
||||
params.Add(jsonutils.NewString(args.NAME), "name")
|
||||
|
||||
@@ -170,7 +170,7 @@ func init() {
|
||||
AutoCreateProject bool `help:"automatically create a default project when importing domain" json:"-"`
|
||||
NoAutoCreateProject bool `help:"do not create default project when importing domain" json:"-"`
|
||||
|
||||
TargetDomain string `help:"target domain without creating new domain"`
|
||||
TargetDomain string `help:"target domain without creating new domain" json:"-"`
|
||||
|
||||
api.SLDAPIdpConfigSingleDomainOptions
|
||||
}
|
||||
@@ -245,4 +245,53 @@ func init() {
|
||||
return nil
|
||||
})
|
||||
|
||||
type IdentityProviderCreateCASOptions struct {
|
||||
NAME string `help:"name of identity provider" json:"-"`
|
||||
|
||||
AutoCreateProject bool `help:"automatically create a default project when importing domain" json:"-"`
|
||||
NoAutoCreateProject bool `help:"do not create default project when importing domain" json:"-"`
|
||||
|
||||
TargetDomain string `help:"target domain without creating new domain" json:"-"`
|
||||
|
||||
api.SCASIdpConfigOptions
|
||||
}
|
||||
R(&IdentityProviderCreateCASOptions{}, "idp-create-cas", "Create an identity provider with CAS driver", func(s *mcclient.ClientSession, args *IdentityProviderCreateCASOptions) error {
|
||||
params := jsonutils.NewDict()
|
||||
params.Add(jsonutils.NewString(args.NAME), "name")
|
||||
|
||||
if len(args.TargetDomain) > 0 {
|
||||
params.Add(jsonutils.NewString(args.TargetDomain), "target_domain")
|
||||
}
|
||||
if args.AutoCreateProject {
|
||||
params.Add(jsonutils.JSONTrue, "auto_create_project")
|
||||
} else if args.NoAutoCreateProject {
|
||||
params.Add(jsonutils.JSONFalse, "auto_create_project")
|
||||
}
|
||||
|
||||
params.Add(jsonutils.NewString("cas"), "driver")
|
||||
params.Add(jsonutils.Marshal(args), "config", "cas")
|
||||
|
||||
idp, err := modules.IdentityProviders.Create(s, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(idp)
|
||||
return nil
|
||||
})
|
||||
|
||||
type IdentityProviderConfigCASOptions struct {
|
||||
ID string `help:"ID of idp to config" json:"-"`
|
||||
api.SCASIdpConfigOptions
|
||||
}
|
||||
R(&IdentityProviderConfigCASOptions{}, "idp-config-cas", "Config an Identity provider with CAS driver", func(s *mcclient.ClientSession, args *IdentityProviderConfigCASOptions) error {
|
||||
config := jsonutils.NewDict()
|
||||
config.Add(jsonutils.Marshal(args), "config", "cas")
|
||||
nconf, err := modules.IdentityProviders.PerformAction(s, args.ID, "config", config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(nconf.PrettyString())
|
||||
return nil
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@@ -72,7 +72,6 @@ func (h *AuthHandlers) AddMethods() {
|
||||
NewHP(h.validatePasscode, "passcode"),
|
||||
NewHP(h.resetTotpRecoveryQuestions, "recovery"),
|
||||
NewHP(h.postLoginHandler, "login"),
|
||||
NewHP(h.postLogoutHandler, "logout"),
|
||||
)
|
||||
|
||||
// auth middleware handler
|
||||
@@ -81,6 +80,7 @@ func (h *AuthHandlers) AddMethods() {
|
||||
NewHP(h.getPermissionDetails, "permissions"),
|
||||
NewHP(h.getAdminResources, "admin_resources"),
|
||||
NewHP(h.getResources, "scoped_resources"),
|
||||
NewHP(h.postLogoutHandler, "logout"),
|
||||
)
|
||||
h.AddByMethod(POST, FetchAuthToken,
|
||||
NewHP(h.resetUserPassword, "password"),
|
||||
@@ -111,7 +111,9 @@ func (h *AuthHandlers) GetRegionsResponse(ctx context.Context, w http.ResponseWr
|
||||
}
|
||||
regionsJson := jsonutils.NewStringArray(regions)
|
||||
s := auth.GetAdminSession(ctx, regions[0], "")
|
||||
result, e := modules.Domains.List(s, nil)
|
||||
filters := jsonutils.NewDict()
|
||||
filters.Add(jsonutils.NewInt(1000), "limit")
|
||||
result, e := modules.Domains.List(s, filters)
|
||||
if e != nil {
|
||||
return nil, errors.Wrap(e, "list domain")
|
||||
}
|
||||
@@ -127,6 +129,33 @@ func (h *AuthHandlers) GetRegionsResponse(ctx context.Context, w http.ResponseWr
|
||||
resp := jsonutils.NewDict()
|
||||
resp.Add(domains, "domains")
|
||||
resp.Add(regionsJson, "regions")
|
||||
|
||||
filters = jsonutils.NewDict()
|
||||
filters.Add(jsonutils.NewStringArray([]string{"cas"}), "driver")
|
||||
filters.Add(jsonutils.NewInt(1000), "limit")
|
||||
idps, err := modules.IdentityProviders.List(s, filters)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "list idp")
|
||||
}
|
||||
retIdps := make([]jsonutils.JSONObject, 0)
|
||||
for i := range idps.Data {
|
||||
retIdp := jsonutils.NewDict()
|
||||
id, _ := idps.Data[i].GetString("id")
|
||||
name, _ := idps.Data[i].GetString("name")
|
||||
driver, _ := idps.Data[i].GetString("driver")
|
||||
retIdp.Add(jsonutils.NewString(id), "id")
|
||||
retIdp.Add(jsonutils.NewString(name), "name")
|
||||
retIdp.Add(jsonutils.NewString(driver), "driver")
|
||||
conf, err := modules.IdentityProviders.GetSpecific(s, id, "config", nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "idp get config spec")
|
||||
}
|
||||
retIdp.Update(conf)
|
||||
retIdps = append(retIdps, retIdp)
|
||||
}
|
||||
|
||||
resp.Add(jsonutils.NewArray(retIdps...), "idps")
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
@@ -218,62 +247,51 @@ func isUserEnableTotp(ctx context.Context, req *http.Request, token mcclient.Tok
|
||||
return jsonutils.QueryBoolean(usr, "enable_mfa", true)
|
||||
}
|
||||
|
||||
func (h *AuthHandlers) doPasswordLogin(ctx context.Context, w http.ResponseWriter, req *http.Request, uname string, body jsonutils.JSONObject) mcclient.TokenCredential {
|
||||
if h.preLoginHook != nil {
|
||||
if err := h.preLoginHook(ctx, req, uname, body); err != nil {
|
||||
httperrors.GeneralServerError(w, err)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
passwd, e := body.GetString("password")
|
||||
if e != nil {
|
||||
httperrors.InvalidInputError(w, "get password in body")
|
||||
return nil
|
||||
}
|
||||
if len(uname) == 0 || len(passwd) == 0 {
|
||||
httperrors.InvalidInputError(w, "username or password is empty")
|
||||
return nil
|
||||
}
|
||||
|
||||
tenant, uname := parseLoginUser(uname)
|
||||
// var token mcclient.TokenCredential
|
||||
domain, _ := body.GetString("domain")
|
||||
func (h *AuthHandlers) doCredentialLogin(ctx context.Context, req *http.Request, body jsonutils.JSONObject) (mcclient.TokenCredential, error) {
|
||||
var token mcclient.TokenCredential
|
||||
var err error
|
||||
var tenant string
|
||||
cliIp := netutils2.GetHttpRequestIp(req)
|
||||
token, err := auth.Client().AuthenticateWeb(uname, passwd, domain, "", "", cliIp)
|
||||
if body.Contains("username") {
|
||||
uname, _ := body.GetString("username")
|
||||
|
||||
if h.preLoginHook != nil {
|
||||
if err := h.preLoginHook(ctx, req, uname, body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
passwd, err := body.GetString("password")
|
||||
if err != nil {
|
||||
return nil, httperrors.NewInputParameterError("get password in body")
|
||||
}
|
||||
if len(uname) == 0 || len(passwd) == 0 {
|
||||
return nil, httperrors.NewInputParameterError("username or password is empty")
|
||||
}
|
||||
|
||||
tenant, uname = parseLoginUser(uname)
|
||||
// var token mcclient.TokenCredential
|
||||
domain, _ := body.GetString("domain")
|
||||
token, err = auth.Client().AuthenticateWeb(uname, passwd, domain, "", "", cliIp)
|
||||
} else if body.Contains("cas_ticket") {
|
||||
ticket, _ := body.GetString("cas_ticket")
|
||||
if len(ticket) == 0 {
|
||||
return nil, httperrors.NewInputParameterError("cas_ticket is empty")
|
||||
}
|
||||
token, err = auth.Client().AuthenticateCAS(ticket, "", "", "", cliIp)
|
||||
} else {
|
||||
return nil, httperrors.NewInputParameterError("missing credential")
|
||||
}
|
||||
if err != nil {
|
||||
switch httperr := err.(type) {
|
||||
case *httputils.JSONClientError:
|
||||
if httperr.Code == 409 {
|
||||
httperrors.GeneralServerError(w, err)
|
||||
return nil
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
httperrors.InvalidCredentialError(w, "username/password incorrect")
|
||||
return nil
|
||||
return nil, httperrors.NewInvalidCredentialError("username/password incorrect")
|
||||
}
|
||||
/* if token == nil { // invalid domain, try all domains
|
||||
s := auth.GetAdminSession(ctx, fetchRegion(req), "")
|
||||
domains, e := modules.Domains.List(s, nil)
|
||||
if e != nil {
|
||||
httperrors.InternalServerError(w, "获取认证源列表失败")
|
||||
return nil
|
||||
}
|
||||
for _, d := range domains.Data {
|
||||
domain, e = d.GetString("name")
|
||||
if e == nil {
|
||||
token, e = auth.Client().Authenticate(uname, passwd, domain, "")
|
||||
if e == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if token == nil {
|
||||
httperrors.InvalidCredentialError(w, "用户名/密码错误")
|
||||
return nil
|
||||
}
|
||||
*/
|
||||
uname := token.GetUserName()
|
||||
if len(tenant) > 0 {
|
||||
s := auth.GetAdminSession(ctx, FetchRegion(req), "")
|
||||
jsonProj, e := modules.Projects.GetById(s, tenant, nil)
|
||||
@@ -317,7 +335,7 @@ func (h *AuthHandlers) doPasswordLogin(ctx context.Context, w http.ResponseWrite
|
||||
log.Errorf("GetProjects for login user error %s project count %d", e, len(projects.Data))
|
||||
}
|
||||
}
|
||||
return token
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func parseLoginUser(uname string) (string, string) {
|
||||
@@ -391,13 +409,21 @@ func (h *AuthHandlers) postLoginHandler(ctx context.Context, w http.ResponseWrit
|
||||
httperrors.InvalidInputError(w, "fetch json for request: %v", e)
|
||||
return
|
||||
}
|
||||
uname, e := body.GetString("username")
|
||||
var token mcclient.TokenCredential
|
||||
otpVerified := false
|
||||
if e != nil { // switch project
|
||||
if body.Contains("tenantId") { // switch project
|
||||
token, otpVerified = doTenantLogin(ctx, w, req, body)
|
||||
} else { // user/password authenticate
|
||||
token = h.doPasswordLogin(ctx, w, req, uname, body)
|
||||
} else if body.Contains("username") || body.Contains("cas_ticket") {
|
||||
// user/password authenticate
|
||||
// cas authentication
|
||||
token, e = h.doCredentialLogin(ctx, req, body)
|
||||
if e != nil {
|
||||
httperrors.GeneralServerError(w, e)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
httperrors.InvalidInputError(w, "no login credential")
|
||||
return
|
||||
}
|
||||
if token == nil {
|
||||
return
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright 2019 Yunion
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package identity
|
||||
|
||||
type SCASIdpConfigOptions struct {
|
||||
// https://cas.example.org/cas/
|
||||
CASServerURL string `json:"cas_server_url"`
|
||||
Service string `json:"service"`
|
||||
}
|
||||
@@ -31,6 +31,7 @@ const (
|
||||
AUTH_METHOD_PASSWORD = "password"
|
||||
AUTH_METHOD_TOKEN = "token"
|
||||
AUTH_METHOD_AKSK = "aksk"
|
||||
AUTH_METHOD_CAS = "cas"
|
||||
|
||||
// AUTH_METHOD_ID_PASSWORD = 1
|
||||
// AUTH_METHOD_ID_TOKEN = 2
|
||||
@@ -56,6 +57,7 @@ const (
|
||||
|
||||
IdentityDriverSQL = "sql"
|
||||
IdentityDriverLDAP = "ldap"
|
||||
IdentityDriverCAS = "cas"
|
||||
|
||||
IdentityDriverStatusConnected = "connected"
|
||||
IdentityDriverStatusDisconnected = "disconnected"
|
||||
@@ -74,7 +76,7 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
AUTH_METHODS = []string{AUTH_METHOD_PASSWORD, AUTH_METHOD_TOKEN, AUTH_METHOD_AKSK}
|
||||
AUTH_METHODS = []string{AUTH_METHOD_PASSWORD, AUTH_METHOD_TOKEN, AUTH_METHOD_AKSK, AUTH_METHOD_CAS}
|
||||
|
||||
SensitiveDomainConfigMap = map[string]string{
|
||||
"ldap": "password",
|
||||
|
||||
@@ -37,6 +37,11 @@ func Send(w http.ResponseWriter, text string) {
|
||||
sendBytes(w, []byte(text))
|
||||
}
|
||||
|
||||
func SendHTML(w http.ResponseWriter, text string) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
sendBytes(w, []byte(text))
|
||||
}
|
||||
|
||||
func sendBytes(w http.ResponseWriter, output []byte) {
|
||||
w.Header().Set("Content-Length", strconv.FormatInt(int64(len(output)), 10))
|
||||
w.Write(output)
|
||||
|
||||
@@ -20,10 +20,10 @@ import (
|
||||
|
||||
"yunion.io/x/jsonutils"
|
||||
|
||||
"yunion.io/x/onecloud/pkg/httperrors"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db/taskman"
|
||||
"yunion.io/x/onecloud/pkg/cloudprovider"
|
||||
"yunion.io/x/onecloud/pkg/compute/models"
|
||||
"yunion.io/x/onecloud/pkg/httperrors"
|
||||
"yunion.io/x/onecloud/pkg/mcclient"
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
// Copyright 2019 Yunion
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cas
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
|
||||
"yunion.io/x/jsonutils"
|
||||
"yunion.io/x/log"
|
||||
"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"
|
||||
"yunion.io/x/onecloud/pkg/util/httputils"
|
||||
)
|
||||
|
||||
// apereo CAS (Central Authentication Server)
|
||||
type SCASDriver struct {
|
||||
driver.SBaseIdentityDriver
|
||||
|
||||
casConfig *api.SCASIdpConfigOptions
|
||||
|
||||
isDebug bool
|
||||
}
|
||||
|
||||
func NewCASDriver(idpId, idpName, template, targetDomainId string, autoCreateProject bool, conf api.TIdentityProviderConfigs) (driver.IIdentityBackend, error) {
|
||||
base, err := driver.NewBaseIdentityDriver(idpId, idpName, template, targetDomainId, autoCreateProject, conf)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "NewBaseIdentityDriver")
|
||||
}
|
||||
drv := SCASDriver{SBaseIdentityDriver: base}
|
||||
drv.SetVirtualObject(&drv)
|
||||
err = drv.prepareConfig()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "prepareConfig")
|
||||
}
|
||||
return &drv, nil
|
||||
}
|
||||
|
||||
func (self *SCASDriver) prepareConfig() error {
|
||||
if self.casConfig == nil {
|
||||
conf := api.SCASIdpConfigOptions{}
|
||||
confJson := jsonutils.Marshal(self.Config["cas"])
|
||||
err := confJson.Unmarshal(&conf)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "json.Unmarshal")
|
||||
}
|
||||
log.Debugf("%s %s %#v", self.Config, confJson, self.casConfig)
|
||||
self.casConfig = &conf
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *SCASDriver) request(ctx context.Context, method httputils.THttpMethod, path string) ([]byte, error) {
|
||||
cli := httputils.GetDefaultClient()
|
||||
urlStr := httputils.JoinPath(self.casConfig.CASServerURL, path)
|
||||
resp, err := httputils.Request(cli, ctx, method, urlStr, nil, nil, self.isDebug)
|
||||
_, body, err := httputils.ParseResponse(resp, err, self.isDebug)
|
||||
return body, err
|
||||
}
|
||||
|
||||
/*
|
||||
serviceValidate response:
|
||||
<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
|
||||
<cas:authenticationSuccess>
|
||||
<cas:user>casuser</cas:user>
|
||||
</cas:authenticationSuccess>
|
||||
</cas:serviceResponse>
|
||||
<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
|
||||
<cas:authenticationSuccess>
|
||||
<cas:user>casuser</cas:user>
|
||||
<cas:attributes>
|
||||
<cas:credentialType>UsernamePasswordCredential</cas:credentialType>
|
||||
<cas:isFromNewLogin>false</cas:isFromNewLogin>
|
||||
<cas:authenticationDate>2019-09-05T12:40:08.014Z[UTC]</cas:authenticationDate>
|
||||
<cas:authenticationMethod>AcceptUsersAuthenticationHandler</cas:authenticationMethod>
|
||||
<cas:successfulAuthenticationHandlers>AcceptUsersAuthenticationHandler</cas:successfulAuthenticationHandlers>
|
||||
<cas:longTermAuthenticationRequestTokenUsed>false</cas:longTermAuthenticationRequestTokenUsed>
|
||||
</cas:attributes>
|
||||
</cas:authenticationSuccess>
|
||||
</cas:serviceResponse>
|
||||
*/
|
||||
type SCASServiceResponse struct {
|
||||
XMLName xml.Name `xml:"serviceResponse"`
|
||||
CASAuthenticationSuccess struct {
|
||||
CASUser string `xml:"user"`
|
||||
} `xml:"authenticationSuccess"`
|
||||
}
|
||||
|
||||
func (self *SCASDriver) Authenticate(ctx context.Context, ident mcclient.SAuthenticationIdentity) (*api.SUserExtended, error) {
|
||||
query := jsonutils.NewDict()
|
||||
query.Set("ticket", jsonutils.NewString(ident.CASTicket.Id))
|
||||
query.Set("service", jsonutils.NewString(self.casConfig.Service))
|
||||
path := "serviceValidate?" + query.QueryString()
|
||||
resp, err := self.request(ctx, "GET", path)
|
||||
/*if err != nil && httputils.ErrorCode(err) == 404 {
|
||||
path = "serviceValidate?" + query.QueryString()
|
||||
resp, err = self.request(ctx, "GET", path)
|
||||
}*/
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "self.request")
|
||||
}
|
||||
log.Debugf("%s", resp)
|
||||
casResp := SCASServiceResponse{}
|
||||
err = xml.Unmarshal(resp, &casResp)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "xml.Unmarshal")
|
||||
}
|
||||
log.Debugf("%s", jsonutils.Marshal(&casResp))
|
||||
usrId := casResp.CASAuthenticationSuccess.CASUser
|
||||
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, err := idp.GetSingleDomain(ctx, api.DefaultRemoteDomainId, self.IdpName, fmt.Sprintf("cas provider %s", self.IdpName))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "idp.GetSingleDomain")
|
||||
}
|
||||
usr, err := idp.SyncOrCreateUser(ctx, usrId, usrId, domain.Id, 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")
|
||||
}
|
||||
return extUser, nil
|
||||
}
|
||||
|
||||
func (self *SCASDriver) Sync(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *SCASDriver) Probe(ctx context.Context) error {
|
||||
_, err := self.request(ctx, "GET", "login")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "self.request")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright 2019 Yunion
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cas
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestXmlUnmarshal(t *testing.T) {
|
||||
xmlstr := `<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
|
||||
<cas:authenticationSuccess>
|
||||
<cas:user>casuser</cas:user>
|
||||
</cas:authenticationSuccess>
|
||||
</cas:serviceResponse>
|
||||
<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
|
||||
<cas:authenticationSuccess>
|
||||
<cas:user>casuser</cas:user>
|
||||
<cas:attributes>
|
||||
<cas:credentialType>UsernamePasswordCredential</cas:credentialType>
|
||||
<cas:isFromNewLogin>false</cas:isFromNewLogin>
|
||||
<cas:authenticationDate>2019-09-05T12:40:08.014Z[UTC]</cas:authenticationDate>
|
||||
<cas:authenticationMethod>AcceptUsersAuthenticationHandler</cas:authenticationMethod>
|
||||
<cas:successfulAuthenticationHandlers>AcceptUsersAuthenticationHandler</cas:successfulAuthenticationHandlers>
|
||||
<cas:longTermAuthenticationRequestTokenUsed>false</cas:longTermAuthenticationRequestTokenUsed>
|
||||
</cas:attributes>
|
||||
</cas:authenticationSuccess>
|
||||
</cas:serviceResponse>`
|
||||
casresp := SCASServiceResponse{}
|
||||
err := xml.Unmarshal([]byte(xmlstr), &casresp)
|
||||
if err != nil {
|
||||
t.Errorf("fail to unmarshal %s", err)
|
||||
} else {
|
||||
t.Logf("%#v", casresp)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright 2019 Yunion
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cas
|
||||
|
||||
import (
|
||||
api "yunion.io/x/onecloud/pkg/apis/identity"
|
||||
"yunion.io/x/onecloud/pkg/keystone/driver"
|
||||
)
|
||||
|
||||
type SCASDriverClass struct{}
|
||||
|
||||
func (self *SCASDriverClass) SingletonInstance() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (self *SCASDriverClass) SyncMethod() string {
|
||||
return api.IdentityProviderSyncOnAuth
|
||||
}
|
||||
|
||||
func (self *SCASDriverClass) NewDriver(idpId, idpName, template, targetDomainId string, autoCreateProject bool, conf api.TIdentityProviderConfigs) (driver.IIdentityBackend, error) {
|
||||
return NewCASDriver(idpId, idpName, template, targetDomainId, autoCreateProject, conf)
|
||||
}
|
||||
|
||||
func (self *SCASDriverClass) Name() string {
|
||||
return api.IdentityDriverCAS
|
||||
}
|
||||
|
||||
func init() {
|
||||
driver.RegisterDriverClass(&SCASDriverClass{})
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright 2019 Yunion
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"yunion.io/x/jsonutils"
|
||||
|
||||
"yunion.io/x/onecloud/pkg/appsrv"
|
||||
"yunion.io/x/onecloud/pkg/httperrors"
|
||||
"yunion.io/x/onecloud/pkg/mcclient"
|
||||
"yunion.io/x/onecloud/pkg/util/httputils"
|
||||
"yunion.io/x/onecloud/pkg/util/netutils2"
|
||||
)
|
||||
|
||||
var (
|
||||
authUrl = "" // http://10.168.222.252:35357/v3
|
||||
casServer = "" // https://cas.example.org/cas
|
||||
serviceUrl = "" // https://app.example.com
|
||||
)
|
||||
|
||||
func defaultPage(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
query, err := jsonutils.ParseQueryString(r.URL.RawQuery)
|
||||
if err != nil {
|
||||
httperrors.BadRequestError(w, "parse query fail %s", err)
|
||||
return
|
||||
}
|
||||
if query.Contains("ticket") {
|
||||
ticket, _ := query.GetString("ticket")
|
||||
service := r.URL.Path
|
||||
referer := r.Header.Get("Referer")
|
||||
cliIp := netutils2.GetHttpRequestIp(r)
|
||||
|
||||
cli := mcclient.NewClient(authUrl, 120, true, true, "", "")
|
||||
token, err := cli.AuthenticateCAS(ticket, "", "", "", cliIp)
|
||||
if err != nil {
|
||||
httperrors.InvalidCredentialError(w, "cas auth error %s", err)
|
||||
return
|
||||
}
|
||||
appsrv.SendHTML(w, fmt.Sprintf("<html><h1>Welcome</h1><h2>[%s]</h2><h2>[%s]</h2><h2>[%s]</h2><h2>[%s]</h2></html>", ticket, service, referer, token.GetUserName()))
|
||||
return
|
||||
} else {
|
||||
httperrors.HTTPError(w, fmt.Sprintf("%s/login?service=%s", casServer, serviceUrl), 302, "Redirect", httputils.Error{})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) <= 6 {
|
||||
fmt.Printf("usage: %s <authUrl> <casServer> <serviceUrl> <certfile> <keyfile> <port>\n", os.Args[0])
|
||||
os.Exit(-1)
|
||||
return
|
||||
}
|
||||
|
||||
authUrl = os.Args[1]
|
||||
casServer = os.Args[2]
|
||||
serviceUrl = os.Args[3]
|
||||
certFile := os.Args[4] // "/etc/yunion/certs/nginx-full.crt"
|
||||
keyFile := os.Args[5] // "/etc/yunion/certs/nginx.key"
|
||||
port := os.Args[6] // 18443
|
||||
|
||||
app := appsrv.NewApplication("casfe", 1, false)
|
||||
app.AddHandler("GET", "/", defaultPage)
|
||||
app.ListenAndServeTLS(fmt.Sprintf("0.0.0.0:%s", port), certFile, keyFile)
|
||||
}
|
||||
@@ -17,7 +17,6 @@ package ldap
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/ldap.v3"
|
||||
|
||||
@@ -26,9 +25,6 @@ import (
|
||||
"yunion.io/x/pkg/tristate"
|
||||
|
||||
api "yunion.io/x/onecloud/pkg/apis/identity"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/consts"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db/lockman"
|
||||
"yunion.io/x/onecloud/pkg/keystone/models"
|
||||
"yunion.io/x/onecloud/pkg/util/ldaputils"
|
||||
)
|
||||
@@ -166,76 +162,11 @@ func (self *SLDAPDriver) syncDomains(ctx context.Context, cli *ldaputils.SLDAPCl
|
||||
}
|
||||
|
||||
func (self *SLDAPDriver) syncDomainInfo(ctx context.Context, info SDomainInfo) (*models.SDomain, error) {
|
||||
domainId, err := models.IdmappingManager.RegisterIdMap(ctx, self.IdpId, info.Id, api.IdMappingEntityDomain)
|
||||
idp, err := models.IdentityProviderManager.FetchIdentityProviderById(self.IdpId)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "IdmappingManager.RegisterIdMap")
|
||||
return nil, errors.Wrap(err, "self.GetIdentityProvider")
|
||||
}
|
||||
|
||||
domain, err := models.DomainManager.FetchDomainById(domainId)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return nil, errors.Wrap(err, "DomainManager.FetchDomainById")
|
||||
}
|
||||
if err == nil {
|
||||
if domain.Name != info.Name {
|
||||
// sync domain name
|
||||
newName, err := db.GenerateName2(models.DomainManager, nil, info.Name, domain)
|
||||
if err != nil {
|
||||
log.Errorf("sync existing domain name (%s=%s) generate fail %s", domain.Name, info.Name, err)
|
||||
} else {
|
||||
_, err = db.Update(domain, func() error {
|
||||
domain.Name = newName
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("sync existing domain name (%s=%s) update fail %s", domain.Name, info.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return domain, nil
|
||||
}
|
||||
|
||||
lockman.LockClass(ctx, models.DomainManager, "")
|
||||
lockman.ReleaseClass(ctx, models.DomainManager, "")
|
||||
|
||||
domain = &models.SDomain{}
|
||||
domain.SetModelManager(models.DomainManager, domain)
|
||||
domain.Id = domainId
|
||||
newName, err := db.GenerateName(models.DomainManager, nil, info.Name)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "GenerateName")
|
||||
}
|
||||
domain.Name = newName
|
||||
domain.Enabled = tristate.True
|
||||
domain.IsDomain = tristate.True
|
||||
domain.DomainId = api.KeystoneDomainRoot
|
||||
domain.Description = fmt.Sprintf("domain for %s", info.DN)
|
||||
err = models.DomainManager.TableSpec().Insert(domain)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "insert")
|
||||
}
|
||||
|
||||
if self.AutoCreateProject && consts.GetNonDefaultDomainProjects() {
|
||||
project := &models.SProject{}
|
||||
project.SetModelManager(models.ProjectManager, project)
|
||||
projectName := models.NormalizeProjectName(fmt.Sprintf("%s_default_project", info.Name))
|
||||
newName, err := db.GenerateName(models.ProjectManager, nil, 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 = fmt.Sprintf("Default project for domain %s", info.Name)
|
||||
project.IsDomain = tristate.False
|
||||
project.ParentId = domain.Id
|
||||
err = models.ProjectManager.TableSpec().Insert(project)
|
||||
if err != nil {
|
||||
log.Errorf("models.ProjectManager.Insert fail %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
return domain, nil
|
||||
return idp.SyncOrCreateDomain(ctx, info.Id, info.Name, info.DN)
|
||||
}
|
||||
|
||||
func (self *SLDAPDriver) syncUsers(ctx context.Context, cli *ldaputils.SLDAPClient, domainId string, baseDN string) (map[string]string, error) {
|
||||
@@ -294,76 +225,32 @@ func (self *SLDAPDriver) syncUsers(ctx context.Context, cli *ldaputils.SLDAPClie
|
||||
return userIdMap, nil
|
||||
}
|
||||
|
||||
func copyUserInfo(ui SUserInfo, userId string, domainId string, user *models.SUser) {
|
||||
user.Id = userId
|
||||
user.Name = ui.Name
|
||||
if ui.Enabled {
|
||||
user.Enabled = tristate.True
|
||||
} else {
|
||||
user.Enabled = tristate.False
|
||||
}
|
||||
user.DomainId = domainId
|
||||
if val, ok := ui.Extra["email"]; ok && len(val) > 0 {
|
||||
user.Email = val
|
||||
}
|
||||
if val, ok := ui.Extra["displayname"]; ok && len(val) > 0 {
|
||||
user.Displayname = val
|
||||
}
|
||||
if val, ok := ui.Extra["mobile"]; ok && len(val) > 0 {
|
||||
user.Mobile = val
|
||||
}
|
||||
}
|
||||
|
||||
func registerNonlocalUser(ctx context.Context, ui SUserInfo, userId string, domainId string) error {
|
||||
lockman.LockRawObject(ctx, models.UserManager.Keyword(), userId)
|
||||
defer lockman.ReleaseRawObject(ctx, models.UserManager.Keyword(), userId)
|
||||
|
||||
userObj, err := db.NewModelObject(models.UserManager)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "db.NewModelObject")
|
||||
}
|
||||
user := userObj.(*models.SUser)
|
||||
q := models.UserManager.RawQuery().Equals("id", userId)
|
||||
err = q.First(user)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return errors.Wrap(err, "Query user")
|
||||
}
|
||||
if err == nil {
|
||||
// update
|
||||
_, err := db.Update(user, func() error {
|
||||
copyUserInfo(ui, userId, domainId, user)
|
||||
user.MarkUnDelete()
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Update")
|
||||
}
|
||||
} else {
|
||||
// new user
|
||||
copyUserInfo(ui, userId, domainId, user)
|
||||
err = models.UserManager.TableSpec().Insert(user)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Insert")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *SLDAPDriver) syncUserDB(ctx context.Context, ui SUserInfo, domainId string) (string, error) {
|
||||
userId, err := models.IdmappingManager.RegisterIdMap(ctx, self.IdpId, ui.Id, api.IdMappingEntityUser)
|
||||
idp, err := models.IdentityProviderManager.FetchIdentityProviderById(self.IdpId)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "models.IdmappingManager.RegisterIdMap")
|
||||
return "", errors.Wrap(err, "models.IdentityProviderManager.FetchIdentityProviderById")
|
||||
}
|
||||
usr, err := idp.SyncOrCreateUser(ctx, ui.Id, ui.Name, domainId, func(user *models.SUser) {
|
||||
if ui.Enabled {
|
||||
user.Enabled = tristate.True
|
||||
} else {
|
||||
user.Enabled = tristate.False
|
||||
}
|
||||
if val, ok := ui.Extra["email"]; ok && len(val) > 0 {
|
||||
user.Email = val
|
||||
}
|
||||
if val, ok := ui.Extra["displayname"]; ok && len(val) > 0 {
|
||||
user.Displayname = val
|
||||
}
|
||||
if val, ok := ui.Extra["mobile"]; ok && len(val) > 0 {
|
||||
user.Mobile = val
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "idp.SyncOrCreateUser")
|
||||
}
|
||||
|
||||
log.Debugf("syncUserDB: %s", userId)
|
||||
|
||||
// insert nonlocal user
|
||||
err = registerNonlocalUser(ctx, ui, userId, domainId)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "registerNonlocalUser")
|
||||
}
|
||||
|
||||
return userId, nil
|
||||
return usr.Id, nil
|
||||
}
|
||||
|
||||
func (self *SLDAPDriver) syncGroups(ctx context.Context, cli *ldaputils.SLDAPClient, domainId string, baseDN string, userIdMap map[string]string) error {
|
||||
|
||||
@@ -17,6 +17,7 @@ package models
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"yunion.io/x/jsonutils"
|
||||
@@ -26,7 +27,9 @@ import (
|
||||
"yunion.io/x/sqlchemy"
|
||||
|
||||
api "yunion.io/x/onecloud/pkg/apis/identity"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/consts"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db/lockman"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db/taskman"
|
||||
"yunion.io/x/onecloud/pkg/httperrors"
|
||||
"yunion.io/x/onecloud/pkg/keystone/driver"
|
||||
@@ -351,11 +354,14 @@ func (ident *SIdentityProvider) PostCreate(ctx context.Context, userCred mcclien
|
||||
return
|
||||
}
|
||||
|
||||
func (manager *SIdentityProviderManager) fetchEnabledProviders() ([]SIdentityProvider, error) {
|
||||
func (manager *SIdentityProviderManager) FetchEnabledProviders(driver string) ([]SIdentityProvider, error) {
|
||||
q := manager.Query().IsTrue("enabled")
|
||||
if len(driver) > 0 {
|
||||
q = q.Equals("driver", driver)
|
||||
}
|
||||
providers := make([]SIdentityProvider, 0)
|
||||
err := db.FetchModelObjects(manager, q, &providers)
|
||||
if err != nil {
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return nil, errors.Wrap(err, "FetchModelObjects")
|
||||
}
|
||||
return providers, nil
|
||||
@@ -670,3 +676,146 @@ func (self *SIdentityProvider) startDeleteIdentityProviderTask(ctx context.Conte
|
||||
task.ScheduleRun(nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *SIdentityProvider) GetSingleDomain(ctx context.Context, extId string, extName string, extDesc string) (*SDomain, error) {
|
||||
if len(self.TargetDomainId) > 0 {
|
||||
targetDomain, err := DomainManager.FetchDomainById(self.TargetDomainId)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return nil, errors.Wrap(err, "DomainManager.FetchDomainById")
|
||||
}
|
||||
if targetDomain == nil {
|
||||
log.Warningln("target domain not exist!")
|
||||
} else {
|
||||
return targetDomain, nil
|
||||
}
|
||||
}
|
||||
return self.SyncOrCreateDomain(ctx, extId, extName, extDesc)
|
||||
}
|
||||
|
||||
func (self *SIdentityProvider) SyncOrCreateDomain(ctx context.Context, extId string, extName string, extDesc string) (*SDomain, error) {
|
||||
domainId, err := IdmappingManager.RegisterIdMap(ctx, self.Id, extId, api.IdMappingEntityDomain)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "IdmappingManager.RegisterIdMap")
|
||||
}
|
||||
domain, err := DomainManager.FetchDomainById(domainId)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return nil, errors.Wrap(err, "DomainManager.FetchDomainById")
|
||||
}
|
||||
if err == nil {
|
||||
if domain.Name != extName {
|
||||
// sync domain name
|
||||
newName, err := db.GenerateName2(DomainManager, nil, extName, domain)
|
||||
if err != nil {
|
||||
log.Errorf("sync existing domain name (%s=%s) generate fail %s", domain.Name, extName, err)
|
||||
} else {
|
||||
_, err = db.Update(domain, func() error {
|
||||
domain.Name = newName
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("sync existing domain name (%s=%s) update fail %s", domain.Name, extName, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return domain, nil
|
||||
}
|
||||
|
||||
lockman.LockClass(ctx, DomainManager, "")
|
||||
lockman.ReleaseClass(ctx, DomainManager, "")
|
||||
|
||||
domain = &SDomain{}
|
||||
domain.SetModelManager(DomainManager, domain)
|
||||
domain.Id = domainId
|
||||
newName, err := db.GenerateName(DomainManager, nil, extName)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "GenerateName")
|
||||
}
|
||||
domain.Name = newName
|
||||
domain.Enabled = tristate.True
|
||||
domain.IsDomain = tristate.True
|
||||
domain.DomainId = api.KeystoneDomainRoot
|
||||
domain.Description = fmt.Sprintf("domain for %s", extDesc)
|
||||
err = DomainManager.TableSpec().Insert(domain)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "insert")
|
||||
}
|
||||
|
||||
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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
return domain, nil
|
||||
}
|
||||
|
||||
func (self *SIdentityProvider) SyncOrCreateUser(ctx context.Context, extId string, extName string, domainId string, syncUserInfo func(*SUser)) (*SUser, error) {
|
||||
userId, err := IdmappingManager.RegisterIdMap(ctx, self.Id, extId, api.IdMappingEntityUser)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "IdmappingManager.RegisterIdMap")
|
||||
}
|
||||
|
||||
lockman.LockRawObject(ctx, UserManager.Keyword(), userId)
|
||||
defer lockman.ReleaseRawObject(ctx, UserManager.Keyword(), userId)
|
||||
|
||||
userObj, err := db.NewModelObject(UserManager)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "db.NewModelObject")
|
||||
}
|
||||
user := userObj.(*SUser)
|
||||
q := UserManager.RawQuery().Equals("id", userId)
|
||||
err = q.First(user)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return nil, errors.Wrap(err, "Query user")
|
||||
}
|
||||
if err == nil {
|
||||
// update
|
||||
_, err := db.Update(user, func() error {
|
||||
if syncUserInfo != nil {
|
||||
syncUserInfo(user)
|
||||
}
|
||||
user.Name = extName
|
||||
user.DomainId = domainId
|
||||
user.MarkUnDelete()
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Update")
|
||||
}
|
||||
} else {
|
||||
if syncUserInfo != nil {
|
||||
syncUserInfo(user)
|
||||
}
|
||||
user.Id = userId
|
||||
user.Name = extName
|
||||
user.DomainId = domainId
|
||||
err = UserManager.TableSpec().Insert(user)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Insert")
|
||||
}
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (manager *SIdentityProviderManager) FetchIdentityProviderById(idstr string) (*SIdentityProvider, error) {
|
||||
obj, err := manager.FetchById(idstr)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "manager.FetchById")
|
||||
}
|
||||
return obj.(*SIdentityProvider), nil
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import (
|
||||
)
|
||||
|
||||
func AutoSyncIdentityProviderTask(ctx context.Context, userCred mcclient.TokenCredential, isStart bool) {
|
||||
idps, err := IdentityProviderManager.fetchEnabledProviders()
|
||||
idps, err := IdentityProviderManager.FetchEnabledProviders("")
|
||||
if err != nil {
|
||||
log.Errorf("FetchEnabledProviders fail %s", err)
|
||||
return
|
||||
@@ -65,6 +65,10 @@ func syncIdentityProvider(ctx context.Context, userCred mcclient.TokenCredential
|
||||
log.Debugf("IDP %s is local, no need to sync", idp.Name)
|
||||
return nil
|
||||
}
|
||||
if drvCls.SyncMethod() == api.IdentityProviderSyncOnAuth {
|
||||
log.Debugf("IDP %s sync on auth, no need to sync", idp.Name)
|
||||
return nil
|
||||
}
|
||||
submitIdpSyncTask(ctx, userCred, idp)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ import (
|
||||
"yunion.io/x/onecloud/pkg/mcclient/auth"
|
||||
"yunion.io/x/onecloud/pkg/util/logclient"
|
||||
|
||||
_ "yunion.io/x/onecloud/pkg/keystone/driver/cas"
|
||||
_ "yunion.io/x/onecloud/pkg/keystone/driver/ldap"
|
||||
_ "yunion.io/x/onecloud/pkg/keystone/driver/sql"
|
||||
_ "yunion.io/x/onecloud/pkg/keystone/tasks"
|
||||
|
||||
@@ -164,6 +164,40 @@ func authUserByIdentity(ctx context.Context, ident mcclient.SAuthenticationIdent
|
||||
return usr, nil
|
||||
}
|
||||
|
||||
func authUserByCASV3(ctx context.Context, input mcclient.SAuthenticationInputV3) (*api.SUserExtended, error) {
|
||||
idps, err := models.IdentityProviderManager.FetchEnabledProviders(api.IdentityDriverCAS)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "models.fetchEnabledProviders")
|
||||
}
|
||||
if len(idps) == 0 {
|
||||
return nil, errors.Error("No cas identity provider")
|
||||
}
|
||||
if len(idps) > 1 {
|
||||
return nil, errors.Error("more than 1 cas identity providers?")
|
||||
}
|
||||
idp := &idps[0]
|
||||
conf, err := idp.GetConfig(true)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "idp.GetConfig")
|
||||
}
|
||||
|
||||
backend, err := driver.GetDriver(idp.Driver, idp.Id, idp.Name, idp.Template, idp.TargetDomainId, idp.AutoCreateProject.Bool(), conf)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "driver.GetDriver")
|
||||
}
|
||||
|
||||
usr, err := backend.Authenticate(ctx, input.Auth.Identity)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Authenticate")
|
||||
}
|
||||
|
||||
if idp.Status == api.IdentityDriverStatusDisconnected {
|
||||
idp.MarkConnected(ctx, models.GetDefaultAdminCred())
|
||||
}
|
||||
|
||||
return usr, nil
|
||||
}
|
||||
|
||||
func authUserByAccessKeyV3(ctx context.Context, input mcclient.SAuthenticationInputV3) (*api.SUserExtended, string, api.SAccessKeySecretInfo, error) {
|
||||
var aksk api.SAccessKeySecretInfo
|
||||
|
||||
@@ -227,6 +261,12 @@ func AuthenticateV3(ctx context.Context, input mcclient.SAuthenticationInputV3)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "authUserByAccessKeyV3")
|
||||
}
|
||||
case api.AUTH_METHOD_CAS:
|
||||
// auth by apereo CAS
|
||||
user, err = authUserByCASV3(ctx, input)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "authUserByCASV3")
|
||||
}
|
||||
default:
|
||||
// auth by other methods, password, openid, saml, etc...
|
||||
user, err = authUserByIdentityV3(ctx, input)
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright 2019 Yunion
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mcclient
|
||||
|
||||
import (
|
||||
api "yunion.io/x/onecloud/pkg/apis/identity"
|
||||
"yunion.io/x/onecloud/pkg/httperrors"
|
||||
)
|
||||
|
||||
func (this *Client) AuthenticateCAS(ticket string, projectId, projectName, projectDomain string, cliIp string) (TokenCredential, error) {
|
||||
aCtx := SAuthContext{
|
||||
// CAS auth must comes from Web
|
||||
Source: AuthSourceWeb,
|
||||
Ip: cliIp,
|
||||
}
|
||||
return this.authenticateCASWithContext(ticket, projectId, projectName, projectDomain, aCtx)
|
||||
}
|
||||
|
||||
func (this *Client) authenticateCASWithContext(ticket string, projectId, projectName, projectDomain string, aCtx SAuthContext) (TokenCredential, error) {
|
||||
if this.AuthVersion() != "v3" {
|
||||
return nil, httperrors.ErrNotSupported
|
||||
}
|
||||
input := SAuthenticationInputV3{}
|
||||
input.Auth.Identity.Methods = []string{api.AUTH_METHOD_CAS}
|
||||
input.Auth.Identity.CASTicket.Id = ticket
|
||||
if len(projectId) > 0 {
|
||||
input.Auth.Scope.Project.Id = projectId
|
||||
}
|
||||
if len(projectName) > 0 {
|
||||
input.Auth.Scope.Project.Name = projectName
|
||||
if len(projectDomain) > 0 {
|
||||
input.Auth.Scope.Project.Domain.Name = projectDomain
|
||||
}
|
||||
}
|
||||
input.Auth.Context = aCtx
|
||||
return this._authV3Input(input)
|
||||
}
|
||||
@@ -59,6 +59,10 @@ type SAuthenticationIdentity struct {
|
||||
Id string `json:"id,omitempty"`
|
||||
} `json:"token,omitempty"`
|
||||
AccessKeyRequest string `json:"access_key_secret,omitempty"`
|
||||
|
||||
CASTicket struct {
|
||||
Id string `json:"id,omitempty"`
|
||||
} `json:"cas_ticket,omitempty"`
|
||||
}
|
||||
|
||||
type SAuthenticationInputV3 struct {
|
||||
|
||||
@@ -167,6 +167,10 @@ func (this *Client) _authV3(domainName, uname, passwd, projectId, projectName, p
|
||||
// }
|
||||
}
|
||||
input.Auth.Context = aCtx
|
||||
return this._authV3Input(input)
|
||||
}
|
||||
|
||||
func (this *Client) _authV3Input(input SAuthenticationInputV3) (TokenCredential, error) {
|
||||
hdr, rbody, err := this.jsonRequest(context.Background(), this.authUrl, "", "POST", "/auth/tokens", nil, jsonutils.Marshal(&input))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -250,6 +250,52 @@ func CloseResponse(resp *http.Response) {
|
||||
}
|
||||
}
|
||||
|
||||
func ParseResponse(resp *http.Response, err error, debug bool) (http.Header, []byte, error) {
|
||||
if err != nil {
|
||||
ce := JSONClientError{}
|
||||
ce.Code = 499
|
||||
ce.Details = err.Error()
|
||||
return nil, nil, &ce
|
||||
}
|
||||
defer CloseResponse(resp)
|
||||
if debug {
|
||||
if resp.StatusCode < 300 {
|
||||
green("Status:", resp.StatusCode)
|
||||
green(resp.Header)
|
||||
} else if resp.StatusCode < 400 {
|
||||
yellow("Status:", resp.StatusCode)
|
||||
yellow(resp.Header)
|
||||
} else {
|
||||
red("Status:", resp.StatusCode)
|
||||
red(resp.Header)
|
||||
}
|
||||
}
|
||||
rbody, err := ioutil.ReadAll(resp.Body)
|
||||
if debug {
|
||||
fmt.Fprintf(os.Stderr, "Response body: %s\n", string(rbody))
|
||||
}
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("Fail to read body: %s", err)
|
||||
}
|
||||
if resp.StatusCode < 300 {
|
||||
return resp.Header, rbody, nil
|
||||
} else if resp.StatusCode >= 300 && resp.StatusCode < 400 {
|
||||
ce := JSONClientError{}
|
||||
ce.Code = resp.StatusCode
|
||||
ce.Details = resp.Header.Get("Location")
|
||||
ce.Class = "redirect"
|
||||
return nil, nil, &ce
|
||||
} else {
|
||||
ce := JSONClientError{}
|
||||
ce.Code = resp.StatusCode
|
||||
ce.Details = resp.Status
|
||||
if len(rbody) > 0 {
|
||||
ce.Details = string(rbody)
|
||||
}
|
||||
return nil, nil, &ce
|
||||
}
|
||||
}
|
||||
|
||||
func ParseJSONResponse(resp *http.Response, err error, debug bool) (http.Header, jsonutils.JSONObject, error) {
|
||||
if err != nil {
|
||||
ce := JSONClientError{}
|
||||
@@ -348,3 +394,7 @@ func ParseJSONResponse(resp *http.Response, err error, debug bool) (http.Header,
|
||||
return nil, nil, &ce
|
||||
}
|
||||
}
|
||||
|
||||
func JoinPath(ep string, path string) string {
|
||||
return strings.TrimRight(ep, "/") + "/" + strings.TrimLeft(path, "/")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user