mirror of
https://github.com/yunionio/cloudpods.git
synced 2026-05-08 06:31:00 +08:00
483 lines
15 KiB
Go
483 lines
15 KiB
Go
// 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 handler
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"net/http"
|
|
"net/url"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"yunion.io/x/jsonutils"
|
|
"yunion.io/x/log"
|
|
"yunion.io/x/pkg/appctx"
|
|
"yunion.io/x/pkg/errors"
|
|
"yunion.io/x/pkg/util/httputils"
|
|
"yunion.io/x/pkg/utils"
|
|
|
|
"yunion.io/x/onecloud/pkg/apigateway/options"
|
|
api "yunion.io/x/onecloud/pkg/apis/identity"
|
|
"yunion.io/x/onecloud/pkg/appsrv"
|
|
"yunion.io/x/onecloud/pkg/httperrors"
|
|
"yunion.io/x/onecloud/pkg/mcclient"
|
|
"yunion.io/x/onecloud/pkg/mcclient/auth"
|
|
modules "yunion.io/x/onecloud/pkg/mcclient/modules/identity"
|
|
"yunion.io/x/onecloud/pkg/util/netutils2"
|
|
)
|
|
|
|
func getSsoBaseCallbackUrl() string {
|
|
if options.Options.SsoRedirectUrl == "" {
|
|
return httputils.JoinPath(options.Options.ApiServer, "api/v1/auth/ssologin")
|
|
}
|
|
return options.Options.SsoRedirectUrl
|
|
}
|
|
|
|
func getSsoCallbackUrl(ctx context.Context, req *http.Request, idpId string) string {
|
|
baseUrl := getSsoBaseCallbackUrl()
|
|
s := auth.GetAdminSession(ctx, FetchRegion(req))
|
|
input := api.GetIdpSsoCallbackUriInput{
|
|
RedirectUri: baseUrl,
|
|
}
|
|
resp, err := modules.IdentityProviders.GetSpecific(s, idpId, "sso-callback-uri", jsonutils.Marshal(input))
|
|
if err != nil {
|
|
return baseUrl
|
|
}
|
|
ret, err := resp.GetString("redirect_uri")
|
|
if err != nil {
|
|
return baseUrl
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func getSsoAuthCallbackUrl() string {
|
|
if options.Options.SsoAuthCallbackUrl == "" {
|
|
return httputils.JoinPath(options.Options.ApiServer, "auth")
|
|
}
|
|
return options.Options.SsoAuthCallbackUrl
|
|
}
|
|
|
|
func getSsoLinkCallbackUrl() string {
|
|
if options.Options.SsoLinkCallbackUrl == "" {
|
|
return httputils.JoinPath(options.Options.ApiServer, "user")
|
|
}
|
|
return options.Options.SsoLinkCallbackUrl
|
|
}
|
|
|
|
func getSsoUserNotFoundCallbackUrl() string {
|
|
if options.Options.SsoUserNotFoundCallbackUrl == "" {
|
|
return getSsoAuthCallbackUrl()
|
|
}
|
|
return options.Options.SsoUserNotFoundCallbackUrl
|
|
}
|
|
|
|
func (h *AuthHandlers) getIdpSsoRedirectUri(ctx context.Context, w http.ResponseWriter, req *http.Request) {
|
|
expires := time.Now().Add(time.Minute * 5)
|
|
params := appctx.AppContextParams(ctx)
|
|
idpId := params["<idp_id>"]
|
|
query, _ := jsonutils.ParseQueryString(req.URL.RawQuery)
|
|
var linkuser string
|
|
if query != nil && query.Contains("linkuser") {
|
|
t, _, _ := fetchAuthInfo(ctx, req)
|
|
if t == nil {
|
|
httperrors.InvalidCredentialError(ctx, w, "invalid credential")
|
|
return
|
|
}
|
|
linkuser = t.GetUserId()
|
|
// authCookie := authToken.GetAuthCookie(t)
|
|
// saveCookie(w, constants.YUNION_AUTH_COOKIE, authCookie, "", expires, true)
|
|
}
|
|
|
|
referer := req.Header.Get(http.CanonicalHeaderKey("referer"))
|
|
|
|
if query == nil {
|
|
query = jsonutils.NewDict()
|
|
}
|
|
query.(*jsonutils.JSONDict).Set("idp_nonce", jsonutils.NewString(utils.GenRequestId(4)))
|
|
state := base64.URLEncoding.EncodeToString([]byte(query.String()))
|
|
redirectUri := getSsoCallbackUrl(ctx, req, idpId)
|
|
s := auth.GetAdminSession(ctx, FetchRegion(req))
|
|
input := api.GetIdpSsoRedirectUriInput{
|
|
RedirectUri: redirectUri,
|
|
State: state,
|
|
}
|
|
resp, err := modules.IdentityProviders.GetSpecific(s, idpId, "sso-redirect-uri", jsonutils.Marshal(input))
|
|
if err != nil {
|
|
httperrors.GeneralServerError(ctx, w, err)
|
|
return
|
|
}
|
|
redirUrl, _ := resp.GetString("uri")
|
|
idpDriver, _ := resp.GetString("driver")
|
|
saveCookie(w, "idp_id", idpId, "", expires, true)
|
|
saveCookie(w, "idp_state", state, "", expires, true)
|
|
saveCookie(w, "idp_driver", idpDriver, "", expires, true)
|
|
saveCookie(w, "idp_referer", referer, "", expires, true)
|
|
saveCookie(w, "idp_link_user", linkuser, "", expires, true)
|
|
|
|
appsrv.DisableClientCache(w)
|
|
appsrv.SendRedirect(w, redirUrl)
|
|
}
|
|
|
|
func findExtUserId(input string) string {
|
|
pattern := regexp.MustCompile(`idp.SyncOrCreateDomainAndUser: ([^:]+): UserNotFound`)
|
|
matches := pattern.FindAllStringSubmatch(input, -1)
|
|
log.Debugf("%#v", matches)
|
|
if len(matches) > 0 && len(matches[0]) > 1 {
|
|
return matches[0][1]
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (h *AuthHandlers) handleIdpInitSsoLogin(ctx context.Context, w http.ResponseWriter, req *http.Request) {
|
|
params := appctx.AppContextParams(ctx)
|
|
idpId := params["<idp_id>"]
|
|
s := auth.GetAdminSession(ctx, FetchRegion(req))
|
|
resp, err := modules.IdentityProviders.Get(s, idpId, nil)
|
|
if err != nil {
|
|
httperrors.GeneralServerError(ctx, w, err)
|
|
return
|
|
}
|
|
idpDriver, _ := resp.GetString("driver")
|
|
h.internalSsoLogin(ctx, w, req, idpId, idpDriver)
|
|
}
|
|
|
|
func (h *AuthHandlers) handleSsoLogin(ctx context.Context, w http.ResponseWriter, req *http.Request) {
|
|
|
|
h.internalSsoLogin(ctx, w, req, "", "")
|
|
}
|
|
|
|
func (h *AuthHandlers) internalSsoLogin(ctx context.Context, w http.ResponseWriter, req *http.Request, idpId, idpDriver string) {
|
|
idpIdC := getCookie(req, "idp_id")
|
|
idpDriverC := getCookie(req, "idp_driver")
|
|
idpState := getCookie(req, "idp_state")
|
|
idpReferer := getCookie(req, "idp_referer")
|
|
idpLinkUser := getCookie(req, "idp_link_user")
|
|
|
|
if len(idpIdC) > 0 {
|
|
idpId = idpIdC
|
|
}
|
|
if len(idpDriverC) > 0 {
|
|
idpDriver = idpDriverC
|
|
}
|
|
|
|
for _, k := range []string{"idp_id", "idp_driver", "idp_state", "idp_referer", "idp_link_user"} {
|
|
clearCookie(w, k, "")
|
|
}
|
|
|
|
missing := make([]string, 0)
|
|
if len(idpId) == 0 {
|
|
missing = append(missing, "idp_id")
|
|
}
|
|
if len(idpDriver) == 0 {
|
|
missing = append(missing, "idp_driver")
|
|
}
|
|
/*if len(idpState) == 0 {
|
|
missing = append(missing, "idp_state")
|
|
}
|
|
if len(idpReferer) == 0 {
|
|
missing = append(missing, "idp_referer")
|
|
}*/
|
|
if len(missing) > 0 {
|
|
httperrors.TimeoutError(ctx, w, "session expires, missing %s", strings.Join(missing, ","))
|
|
return
|
|
}
|
|
|
|
var idpStateQs jsonutils.JSONObject
|
|
if len(idpState) > 0 {
|
|
idpStateQsBytes, _ := base64.URLEncoding.DecodeString(idpState)
|
|
idpStateQs, _ = jsonutils.Parse(idpStateQsBytes)
|
|
log.Debugf("state query sting: %s", idpStateQs)
|
|
}
|
|
|
|
var body jsonutils.JSONObject
|
|
var err error
|
|
switch req.Method {
|
|
case "GET":
|
|
body, err = jsonutils.ParseQueryString(req.URL.RawQuery)
|
|
if err != nil {
|
|
httperrors.InputParameterError(ctx, w, "parse query string error: %s", err)
|
|
return
|
|
}
|
|
case "POST":
|
|
formData, err := appsrv.Fetch(req)
|
|
if err != nil {
|
|
httperrors.InputParameterError(ctx, w, "fetch form data error: %s", err)
|
|
}
|
|
body, err = jsonutils.ParseQueryString(string(formData))
|
|
if err != nil {
|
|
httperrors.InputParameterError(ctx, w, "parse form data error: %s", err)
|
|
return
|
|
}
|
|
default:
|
|
httperrors.InputParameterError(ctx, w, "invalid request")
|
|
return
|
|
}
|
|
|
|
body.(*jsonutils.JSONDict).Set("idp_id", jsonutils.NewString(idpId))
|
|
body.(*jsonutils.JSONDict).Set("idp_driver", jsonutils.NewString(idpDriver))
|
|
body.(*jsonutils.JSONDict).Set("idp_state", jsonutils.NewString(idpState))
|
|
|
|
appsrv.DisableClientCache(w)
|
|
|
|
var referer string
|
|
var idpUserId string
|
|
if len(idpLinkUser) > 0 {
|
|
// link with existing user
|
|
err = linkWithExistingUser(ctx, req, idpId, idpLinkUser, body)
|
|
referer = getSsoLinkCallbackUrl()
|
|
} else {
|
|
// ordinary login
|
|
err = h.doLogin(ctx, w, req, body)
|
|
if err != nil {
|
|
if errors.Cause(err) == httperrors.ErrUserNotFound {
|
|
idpUserId = findExtUserId(err.Error())
|
|
if len(idpUserId) == 0 {
|
|
err = httputils.NewJsonClientError(400, string(httperrors.ErrInputParameter), "empty external user id")
|
|
} else {
|
|
referer = getSsoUserNotFoundCallbackUrl()
|
|
}
|
|
}
|
|
}
|
|
if referer == "" {
|
|
referer = getSsoAuthCallbackUrl()
|
|
}
|
|
}
|
|
refererUrl, _ := url.Parse(referer)
|
|
if refererUrl == nil && len(idpReferer) > 0 {
|
|
refererUrl, _ = url.Parse(idpReferer)
|
|
}
|
|
if refererUrl == nil {
|
|
httperrors.InvalidInputError(ctx, w, "empty referer link")
|
|
return
|
|
}
|
|
redirUrl := generateRedirectUrl(refererUrl, idpStateQs, err, idpId, idpUserId)
|
|
appsrv.SendRedirect(w, redirUrl)
|
|
}
|
|
|
|
func generateRedirectUrl(originUrl *url.URL, stateQs jsonutils.JSONObject, err error, idpId, idpUserId string) string {
|
|
var qs jsonutils.JSONObject
|
|
if len(originUrl.RawQuery) > 0 {
|
|
qs, _ = jsonutils.ParseQueryString(originUrl.RawQuery)
|
|
} else {
|
|
qs = jsonutils.NewDict()
|
|
}
|
|
qs.(*jsonutils.JSONDict).Update(stateQs)
|
|
if err != nil {
|
|
var errCls, errDetails string
|
|
switch je := err.(type) {
|
|
case *httputils.JSONClientError:
|
|
errCls = je.Class
|
|
errDetails = je.Details
|
|
default:
|
|
errCls = errors.Cause(err).Error()
|
|
errDetails = err.Error()
|
|
}
|
|
msgLen := 100
|
|
if len(errDetails) > msgLen {
|
|
errDetails = errDetails[:msgLen] + "..."
|
|
}
|
|
qs.(*jsonutils.JSONDict).Add(jsonutils.NewString(errCls), "error_class")
|
|
qs.(*jsonutils.JSONDict).Add(jsonutils.NewString(errDetails), "error_details")
|
|
qs.(*jsonutils.JSONDict).Add(jsonutils.NewString("error"), "result")
|
|
if len(idpUserId) > 0 {
|
|
qs.(*jsonutils.JSONDict).Add(jsonutils.NewString(idpId), "idp_id")
|
|
qs.(*jsonutils.JSONDict).Add(jsonutils.NewString(idpUserId), "idp_entity_id")
|
|
}
|
|
} else {
|
|
qs.(*jsonutils.JSONDict).Add(jsonutils.NewString("success"), "result")
|
|
}
|
|
originUrl.RawQuery = qs.QueryString()
|
|
return originUrl.String()
|
|
}
|
|
|
|
func processSsoLoginData(body jsonutils.JSONObject, cliIp string, redirectUri string) (mcclient.TokenCredential, error) {
|
|
var token mcclient.TokenCredential
|
|
var err error
|
|
idpDriver, _ := body.GetString("idp_driver")
|
|
idpId, _ := body.GetString("idp_id")
|
|
idpState, _ := body.GetString("idp_state")
|
|
switch idpDriver {
|
|
case api.IdentityDriverCAS:
|
|
ticket, _ := body.GetString("ticket")
|
|
if len(ticket) == 0 {
|
|
return nil, httperrors.NewMissingParameterError("ticket")
|
|
}
|
|
token, err = auth.Client().AuthenticateCAS(idpId, ticket, redirectUri, "", "", "", cliIp)
|
|
case api.IdentityDriverSAML:
|
|
samlResp, _ := body.GetString("SAMLResponse")
|
|
relayState, _ := body.GetString("RelayState")
|
|
if relayState != idpState {
|
|
return nil, errors.Wrap(httperrors.ErrInputParameter, "state inconsistent")
|
|
}
|
|
if len(samlResp) == 0 {
|
|
return nil, errors.Wrap(httperrors.ErrMissingParameter, "SAMLResponse")
|
|
}
|
|
token, err = auth.Client().AuthenticateSAML(idpId, samlResp, "", "", "", cliIp)
|
|
case api.IdentityDriverOIDC:
|
|
code, _ := body.GetString("code")
|
|
state, _ := body.GetString("state")
|
|
if state != idpState {
|
|
return nil, errors.Wrap(httperrors.ErrInputParameter, "state inconsistent")
|
|
}
|
|
if len(code) == 0 {
|
|
return nil, errors.Wrap(httperrors.ErrMissingParameter, "code")
|
|
}
|
|
token, err = auth.Client().AuthenticateOIDC(idpId, code, redirectUri, "", "", "", cliIp)
|
|
case api.IdentityDriverOAuth2:
|
|
state, _ := body.GetString("state")
|
|
if state != idpState {
|
|
return nil, errors.Wrap(httperrors.ErrInputParameter, "state inconsistent")
|
|
}
|
|
code, _ := body.GetString("code")
|
|
if len(code) == 0 {
|
|
code, _ = body.GetString("auth_code")
|
|
if len(code) == 0 {
|
|
return nil, errors.Wrap(httperrors.ErrMissingParameter, "code")
|
|
}
|
|
}
|
|
token, err = auth.Client().AuthenticateOAuth2(idpId, code, "", "", "", cliIp)
|
|
default:
|
|
return nil, errors.Wrapf(httperrors.ErrNotSupported, "SSO driver %s not supported", idpDriver)
|
|
}
|
|
return token, err
|
|
}
|
|
|
|
func linkWithExistingUser(ctx context.Context, req *http.Request, idpId, idpLinkUser string, body jsonutils.JSONObject) error {
|
|
t, _, _ := fetchAuthInfo(ctx, req)
|
|
if t == nil {
|
|
return errors.Wrap(httperrors.ErrInvalidCredential, "invalid credential")
|
|
}
|
|
if t.GetUserId() != idpLinkUser {
|
|
return errors.Wrap(httperrors.ErrConflict, "link user id inconsistent with credential")
|
|
}
|
|
cliIp := netutils2.GetHttpRequestIp(req)
|
|
redirectUri := getSsoCallbackUrl(ctx, req, idpId)
|
|
ntoken, err := processSsoLoginData(body, cliIp, redirectUri)
|
|
if err != nil {
|
|
if errors.Cause(err) != httperrors.ErrUserNotFound {
|
|
return errors.Wrap(err, "invalid ssologin result")
|
|
}
|
|
log.Debugf("error: %s", err)
|
|
// not linked, link with user
|
|
// fetch userId
|
|
jsonErr := err.(*httputils.JSONClientError)
|
|
extUserId := findExtUserId(jsonErr.Details)
|
|
if len(extUserId) == 0 {
|
|
return errors.Wrap(httperrors.ErrInputParameter, "empty external user id")
|
|
}
|
|
linkInput := api.UserLinkIdpInput{
|
|
IdpId: idpId,
|
|
IdpEntityId: extUserId,
|
|
}
|
|
s := auth.GetAdminSession(ctx, FetchRegion(req))
|
|
_, err = modules.UsersV3.PerformAction(s, t.GetUserId(), "link-idp", jsonutils.Marshal(linkInput))
|
|
if err != nil {
|
|
return errors.Wrap(err, "link-idp")
|
|
}
|
|
} else {
|
|
if ntoken.GetUserId() != t.GetUserId() {
|
|
return errors.Wrap(httperrors.ErrConflict, "link user id inconsistent with credential")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func handleUnlinkIdp(ctx context.Context, w http.ResponseWriter, req *http.Request) {
|
|
t := AppContextToken(ctx)
|
|
|
|
body, err := appsrv.FetchJSON(req)
|
|
if err != nil {
|
|
httperrors.GeneralServerError(ctx, w, err)
|
|
return
|
|
}
|
|
|
|
idpId, _ := body.GetString("idp_id")
|
|
idpEntityId, _ := body.GetString("idp_entity_id")
|
|
|
|
if len(idpId) == 0 || len(idpEntityId) == 0 {
|
|
httperrors.InputParameterError(ctx, w, "empty idp_id or idp_entity_id")
|
|
return
|
|
}
|
|
|
|
s := auth.GetAdminSession(ctx, FetchRegion(req))
|
|
input := api.UserUnlinkIdpInput{
|
|
IdpId: idpId,
|
|
IdpEntityId: idpEntityId,
|
|
}
|
|
_, err = modules.UsersV3.PerformAction(s, t.GetUserId(), "unlink-idp", jsonutils.Marshal(input))
|
|
if err != nil {
|
|
httperrors.GeneralServerError(ctx, w, err)
|
|
return
|
|
}
|
|
appsrv.Send(w, "")
|
|
}
|
|
|
|
func fetchIdpBasicConfig(ctx context.Context, w http.ResponseWriter, req *http.Request) {
|
|
params := appctx.AppContextParams(ctx)
|
|
idpId := params["<idp_id>"]
|
|
info, err := getIdpBasicConfig(ctx, req, idpId)
|
|
if err != nil {
|
|
httperrors.GeneralServerError(ctx, w, err)
|
|
return
|
|
}
|
|
appsrv.SendJSON(w, info)
|
|
}
|
|
|
|
func getIdpBasicConfig(ctx context.Context, req *http.Request, idpId string) (jsonutils.JSONObject, error) {
|
|
s := auth.GetAdminSession(ctx, FetchRegion(req))
|
|
baseUrl := getSsoBaseCallbackUrl()
|
|
input := api.GetIdpSsoCallbackUriInput{
|
|
RedirectUri: baseUrl,
|
|
}
|
|
resp, err := modules.IdentityProviders.GetSpecific(s, idpId, "sso-callback-uri", jsonutils.Marshal(input))
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "GetSpecific sso-callback-uri")
|
|
}
|
|
redir, _ := resp.GetString("redirect_uri")
|
|
idpDriver, _ := resp.GetString("driver")
|
|
info := jsonutils.NewDict()
|
|
switch idpDriver {
|
|
case api.IdentityDriverSQL:
|
|
case api.IdentityDriverLDAP:
|
|
case api.IdentityDriverCAS:
|
|
info.Add(jsonutils.NewString(redir), "redirect_uri")
|
|
case api.IdentityDriverSAML:
|
|
info.Add(jsonutils.NewString(options.Options.ApiServer), "entity_id")
|
|
info.Add(jsonutils.NewString(redir), "redirect_uri")
|
|
case api.IdentityDriverOIDC:
|
|
info.Add(jsonutils.NewString(redir), "redirect_uri")
|
|
case api.IdentityDriverOAuth2:
|
|
info.Add(jsonutils.NewString(redir), "redirect_uri")
|
|
default:
|
|
}
|
|
return info, nil
|
|
}
|
|
|
|
func fetchIdpSAMLMetadata(ctx context.Context, w http.ResponseWriter, req *http.Request) {
|
|
s := auth.GetAdminSession(ctx, FetchRegion(req))
|
|
params := appctx.AppContextParams(ctx)
|
|
idpId := params["<idp_id>"]
|
|
query := jsonutils.NewDict()
|
|
query.Set("redirect_uri", jsonutils.NewString(getSsoCallbackUrl(ctx, req, idpId)))
|
|
md, err := modules.IdentityProviders.GetSpecific(s, idpId, "saml-metadata", query)
|
|
if err != nil {
|
|
httperrors.GeneralServerError(ctx, w, err)
|
|
return
|
|
}
|
|
appsrv.SendJSON(w, md)
|
|
}
|