feature: keystone support CAS SSO authentication

This commit is contained in:
Qiu Jian
2019-09-06 13:08:02 +08:00
parent 28064c9513
commit 3591afee2e
19 changed files with 824 additions and 202 deletions
+53 -4
View File
@@ -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
})
}
+81 -55
View File
@@ -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
+21
View File
@@ -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"`
}
+3 -1
View File
@@ -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",
+5
View File
@@ -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)
+1 -1
View File
@@ -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"
)
+160
View File
@@ -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
}
+48
View File
@@ -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)
}
}
+42
View File
@@ -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{})
}
+81
View File
@@ -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)
}
+25 -138
View File
@@ -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 {
+151 -2
View File
@@ -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
}
+5 -1
View File
@@ -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
}
+1
View File
@@ -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"
+40
View File
@@ -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)
+49
View File
@@ -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)
}
+4
View File
@@ -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 {
+4
View File
@@ -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
+50
View File
@@ -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, "/")
}