From 627503cdc14bd2dd18dcddee7b7f9bfd735803fe Mon Sep 17 00:00:00 2001 From: Qu Xuan Date: Thu, 7 Jan 2021 20:59:23 +0800 Subject: [PATCH] feat(cloudid): add azure saml --- .../yunion/share/saml/sp-metadata/azure.xml | 60 ++++++++ go.mod | 4 +- go.sum | 8 +- pkg/appsrv/appsrv.go | 63 +++++++- pkg/cloudid/models/cloudaccount.go | 44 +++++- pkg/cloudid/models/cloudgroupresource.go | 2 +- pkg/cloudid/models/samluser.go | 43 ++++++ pkg/cloudid/saml/load.go | 1 + pkg/cloudid/saml/providers/azure/doc.go | 15 ++ pkg/cloudid/saml/providers/azure/driver.go | 131 +++++++++++++++++ pkg/cloudid/saml/providers/azure/factory.go | 38 +++++ pkg/cloudprovider/clouduser.go | 1 + pkg/cloudprovider/resources.go | 3 + pkg/cloudprovider/saml.go | 1 + pkg/mcclient/options/cloudid/cloudgroup.go | 2 +- pkg/multicloud/aliyun/ram_user.go | 8 ++ pkg/multicloud/apsara/ram_user.go | 8 ++ pkg/multicloud/aws/iam_user.go | 8 ++ pkg/multicloud/azure/azure.go | 56 +++++++- pkg/multicloud/azure/cloudpolicy.go | 3 +- pkg/multicloud/azure/clouduser.go | 13 ++ pkg/multicloud/azure/provider/provider.go | 13 ++ pkg/multicloud/azure/saml_provider.go | 136 ++++++++++++++++++ pkg/multicloud/azure/shell/clouduser.go | 12 ++ pkg/multicloud/azure/shell/saml_provider.go | 50 +++++++ pkg/multicloud/google/iampolicy.go | 8 ++ pkg/multicloud/huawei/clouduser.go | 8 ++ pkg/multicloud/qcloud/cam_user.go | 8 ++ pkg/util/samlutils/demo/service.go | 25 ++++ pkg/util/samlutils/idp/idp.go | 12 +- pkg/util/samlutils/idp/sp.go | 2 + pkg/util/samlutils/response.go | 5 +- pkg/util/samlutils/types.go | 2 + pkg/util/samlutils/util.go | 15 +- .../clientcredentials/clientcredentials.go | 120 ++++++++++++++++ vendor/modules.txt | 5 +- .../yunion.io/x/pkg/util/compare/compare.go | 15 ++ 37 files changed, 916 insertions(+), 32 deletions(-) create mode 100644 build/cloudid/root/opt/yunion/share/saml/sp-metadata/azure.xml create mode 100644 pkg/cloudid/saml/providers/azure/doc.go create mode 100644 pkg/cloudid/saml/providers/azure/driver.go create mode 100644 pkg/cloudid/saml/providers/azure/factory.go create mode 100644 pkg/multicloud/azure/saml_provider.go create mode 100644 pkg/multicloud/azure/shell/saml_provider.go create mode 100644 vendor/golang.org/x/oauth2/clientcredentials/clientcredentials.go diff --git a/build/cloudid/root/opt/yunion/share/saml/sp-metadata/azure.xml b/build/cloudid/root/opt/yunion/share/saml/sp-metadata/azure.xml new file mode 100644 index 0000000000..a87535a616 --- /dev/null +++ b/build/cloudid/root/opt/yunion/share/saml/sp-metadata/azure.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + fA33hUaj24/8F8bArDIkH6CpX0k= + + + p/rlVWxLRIYiHozzWUgsjmQ5ItMZ/ITeEOg/edeo50l7qU8wLM9JD55EHoLpVvxImBcCfY1CxKdq3s1o9Up1jvEcH9KjfYZea1q8hzTkQdt7F2XvxoqDFRLvVwyHHJjygaEvFRXXxml7IDgDx9ScSRhQeGUlTGhI9eAFSaGkChM/FPnmekZ3ngSwbVf4FsTLIArxMqEnV0lXNMTr5NHQ6VL5wZ/cHLNCkkBoY0qWyv2hnYiZiXa4TKjpAN+8QF5GDPv/0ExJwe+aoRAQhdBzhBUICQdQd+v3Rb5SuX++iVfmf4IPMqj8CK4uK7iYXNLCGEO8oFR2KlK/zBpHzw6yCQ== + + + MIIC/TCCAeWgAwIBAgIQF9xAVqfHt7dNSOnsMKYLDjANBgkqhkiG9w0BAQsFADApMScwJQYDVQQDEx5MaXZlIElEIFNUUyBTaWduaW5nIFB1YmxpYyBLZXkwHhcNMTkwNjE4MDAwMDAwWhcNMjEwNjE4MDAwMDAwWjApMScwJQYDVQQDEx5MaXZlIElEIFNUUyBTaWduaW5nIFB1YmxpYyBLZXkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDLwL+mOk+zhgjHPFiZuDaU7coCOMkYyJyWS2B2KWYx7BMmy+SBSlFvvDHIiMXeLnv9DiUnQXtjClZXNqEo2amM/KdXfnCUlwdnqGa89TbB8kM6aBQKCh5Up7vejD+KRpPcOK7DqxWnm037zNKH+6liwRMUD/+lkBODoG1/ThuBVM0Kr1Oowfnkb4JYeF5xZk+2+ThR2dcQzdsz8dBW1GpGopdoM5G1amVhoRPBeNlYUqf0JUPE36Dx0iCmeXX00HTR2gd+/DJFVe34l/VxL4cqufmRcqWHy7axonbpt8svjAervGgueF5W/BVGcO4eVwFzFfnAz7bSaH/zbsNnQkFtAgMBAAGjITAfMB0GA1UdDgQWBBQlxmu/Hj1qB2A306p0Sz9up711AjANBgkqhkiG9w0BAQsFAAOCAQEAEhb+i+J69DvzCTfmC0Mn+uSGzBaW0ksMZNjM8/p/RFueytNo8/DY6TUb9W8l/HSVON782iGG7hIsG68RUIZ5q+/WAB9iBvKkKv5aGZ0bK/eD5boHwBwMa3udTjU68Vkai0A7naISEh22h+lQPvKiftCEzK1x9j2FoAXEMhACnpTwRrrj3SsJXCsMaOKcVw7sCmM4/4wD5I1+dfMSe85rSEzzUCDcCMEg67sBKdO2CFK1GoH21ttrz1CV+xt8Pn9a488kgsp2ZyU+yixS59CrM7onEzt1R4miXWAJAtydNHAmmbgKrTxqf67fEXFcrlFAHexi6vvINZWZPD6+J+q6xg== + + + + + + + + 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 + + + + + urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress + urn:mace:shibboleth:1.0:nameIdentifier + urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified + urn:oasis:names:tc:SAML:2.0:nameid-format:transient + urn:oasis:names:tc:SAML:2.0:nameid-format:persistent + + + + + + + + + diff --git a/go.mod b/go.mod index eff836fec8..98afecd26b 100644 --- a/go.mod +++ b/go.mod @@ -141,11 +141,11 @@ require ( k8s.io/apimachinery v0.19.3 k8s.io/client-go v0.19.3 k8s.io/cluster-bootstrap v0.19.3 - yunion.io/x/executor v0.0.0-20201201131200-44fa553abd9e + yunion.io/x/executor v0.0.0-20201231064744-df32f32165a9 yunion.io/x/jsonutils v0.0.0-20201110084044-3e4e1cb49769 yunion.io/x/log v0.0.0-20201210064738-43181789dc74 yunion.io/x/ovsdb v0.0.0-20200526071744-27bf0940cbc7 - yunion.io/x/pkg v0.0.0-20201123083159-ca3aea986ff2 + yunion.io/x/pkg v0.0.0-20210109071527-7e72daf56747 yunion.io/x/s3cli v0.0.0-20190917004522-13ac36d8687e yunion.io/x/sqlchemy v0.0.0-20201219153152-2d901261898c yunion.io/x/structarg v0.0.0-20200720093445-9f850fa222ce diff --git a/go.sum b/go.sum index 82fe3ac03f..82f161e505 100644 --- a/go.sum +++ b/go.sum @@ -918,8 +918,8 @@ sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= -yunion.io/x/executor v0.0.0-20201201131200-44fa553abd9e h1:NRtqiegW4NME0v10rpChORZnJnuAUbpv31R5DsqqczQ= -yunion.io/x/executor v0.0.0-20201201131200-44fa553abd9e/go.mod h1:Uxuou9WQIeJXNpy7t2fPLL0BYLvLiMvGQwY7Qc6aSws= +yunion.io/x/executor v0.0.0-20201231064744-df32f32165a9 h1:TyKy58HHgjkTJYrZgD0haz4bItoC+QwcOQ+GtMuGTjw= +yunion.io/x/executor v0.0.0-20201231064744-df32f32165a9/go.mod h1:Uxuou9WQIeJXNpy7t2fPLL0BYLvLiMvGQwY7Qc6aSws= yunion.io/x/jsonutils v0.0.0-20190625054549-a964e1e8a051/go.mod h1:4N0/RVzsYL3kH3WE/H1BjUQdFiWu50JGCFQuuy+Z634= yunion.io/x/jsonutils v0.0.0-20201110084044-3e4e1cb49769 h1:LIQ4hhLGQuQK+XxlV+8JrKBuL37WUT+5ZTVxBwHOTD4= yunion.io/x/jsonutils v0.0.0-20201110084044-3e4e1cb49769/go.mod h1:p0nyMqGA/apTxxyLIU/o1k4V7Vujl2O6ey30L594sYE= @@ -933,8 +933,8 @@ yunion.io/x/pkg v0.0.0-20190620104149-945c25821dbf/go.mod h1:t6rEGG2sQ4J7DhFxSZV yunion.io/x/pkg v0.0.0-20190628082551-f4033ba2ea30/go.mod h1:t6rEGG2sQ4J7DhFxSZVOTjNd0YO/KlfWQyK1W4tog+E= yunion.io/x/pkg v0.0.0-20200814072949-4f1b541857d6 h1:UarEDTBGkgcgc+nc+PZ75uo9M9+jiOGd5P2B90TxDNw= yunion.io/x/pkg v0.0.0-20200814072949-4f1b541857d6/go.mod h1:t6rEGG2sQ4J7DhFxSZVOTjNd0YO/KlfWQyK1W4tog+E= -yunion.io/x/pkg v0.0.0-20201123083159-ca3aea986ff2 h1:NeCr2J8HjcIuJvEhP0rwWA1UKP8ReOv6HVf5k9YPtyA= -yunion.io/x/pkg v0.0.0-20201123083159-ca3aea986ff2/go.mod h1:t6rEGG2sQ4J7DhFxSZVOTjNd0YO/KlfWQyK1W4tog+E= +yunion.io/x/pkg v0.0.0-20210109071527-7e72daf56747 h1:sTr6mjWW8aAMKkrnnU2SG8EqhLRl/D3rGnBhaP2ksTI= +yunion.io/x/pkg v0.0.0-20210109071527-7e72daf56747/go.mod h1:t6rEGG2sQ4J7DhFxSZVOTjNd0YO/KlfWQyK1W4tog+E= yunion.io/x/s3cli v0.0.0-20190917004522-13ac36d8687e h1:v+EzIadodSwkdZ/7bremd7J8J50Cise/HCylsOJngmo= yunion.io/x/s3cli v0.0.0-20190917004522-13ac36d8687e/go.mod h1:0iFKpOs1y4lbCxeOmq3Xx/0AcQoewVPwj62eRluioEo= yunion.io/x/sqlchemy v0.0.0-20201219153152-2d901261898c h1:71nVDQq1oUjvZknEUNfetdiOB1jZMEfmoQlMUaoPIJs= diff --git a/pkg/appsrv/appsrv.go b/pkg/appsrv/appsrv.go index 81092eb4fe..eed62459b7 100644 --- a/pkg/appsrv/appsrv.go +++ b/pkg/appsrv/appsrv.go @@ -505,18 +505,71 @@ func isJsonContentType(r *http.Request) bool { return false } +func isFormContentType(r *http.Request) bool { + contType := strings.ToLower(r.Header.Get("Content-Type")) + if strings.HasPrefix(contType, "application/json") { + return true + } + return false +} + +type TContentType string + +const ( + ContentTypeJson = TContentType("Json") + ContentTypeForm = TContentType("Form") + ContentTypeUnknown = TContentType("Unknown") +) + +func getContentType(r *http.Request) TContentType { + contType := func() string { + for _, k := range []string{"Content-Type", "content-type"} { + contentType := r.Header.Get(k) + if len(contentType) > 0 { + return strings.ToLower(contentType) + } + } + return "" + }() + for k, v := range map[string]TContentType{ + "application/json": ContentTypeJson, + "application/x-www-form-urlencoded": ContentTypeForm, + } { + if strings.HasPrefix(contType, k) { + return v + } + } + return ContentTypeUnknown +} + func FetchEnv(ctx context.Context, w http.ResponseWriter, r *http.Request) (params map[string]string, query jsonutils.JSONObject, body jsonutils.JSONObject) { var err error params = appctx.AppContextParams(ctx) query, err = jsonutils.ParseQueryString(r.URL.RawQuery) if err != nil { - log.Errorf("Parse query string %s failed: %s", r.URL.RawQuery, err) + log.Errorf("Parse query string %s failed: %v", r.URL.RawQuery, err) } //var body jsonutils.JSONObject = nil - if (r.Method == "PUT" || r.Method == "POST" || r.Method == "DELETE" || r.Method == "PATCH") && r.ContentLength > 0 && isJsonContentType(r) { - body, err = FetchJSON(r) - if err != nil { - log.Errorf("Fail to decode JSON request body: %s", err) + if r.Method == "PUT" || r.Method == "POST" || r.Method == "DELETE" || r.Method == "PATCH" { + switch getContentType(r) { + case ContentTypeJson: + if r.ContentLength > 0 { + body, err = FetchJSON(r) + if err != nil { + log.Warningf("Fail to decode JSON request body: %v", err) + } + } + case ContentTypeForm: + err := r.ParseForm() + if err != nil { + log.Warningf("ParseForm %s error: %v", r.URL.String(), err) + } + query, err = jsonutils.ParseQueryString(r.PostForm.Encode()) + if err != nil { + log.Warningf("Parse query string %s failed: %v", r.PostForm.Encode(), err) + } + default: + log.Warningf("%s invalid contentType with header %v", r.URL.String(), r.Header) } } return params, query, body diff --git a/pkg/cloudid/models/cloudaccount.go b/pkg/cloudid/models/cloudaccount.go index 5f30b88d54..43cc5419c1 100644 --- a/pkg/cloudid/models/cloudaccount.go +++ b/pkg/cloudid/models/cloudaccount.go @@ -557,6 +557,9 @@ func (self *SCloudaccount) GetCloudusers() ([]SClouduser, error) { } func (self *SCloudaccount) SyncCloudusers(ctx context.Context, userCred mcclient.TokenCredential, iUsers []cloudprovider.IClouduser) ([]SClouduser, []cloudprovider.IClouduser, compare.SyncResult) { + lockman.LockRawObject(ctx, "cloudusers", self.Id) + defer lockman.ReleaseRawObject(ctx, "cloudusers", self.Id) + result := compare.SyncResult{} dbUsers, err := self.GetCloudusers() if err != nil { @@ -1147,7 +1150,10 @@ func (self *SCloudaccount) StartSAMLProviderCreateTask(ctx context.Context, user } return nil } - return sp.StartSAMLProviderCreateTask(ctx, userCred, "") + if sp != nil { + return sp.StartSAMLProviderCreateTask(ctx, userCred, "") + } + return nil } func (manager *SCloudaccountManager) SyncSAMLProviders(ctx context.Context, userCred mcclient.TokenCredential, isStart bool) { @@ -1805,3 +1811,39 @@ func (self *SCloudaccount) GetUserCloudgroups(userId string) ([]string, error) { } return ret, nil } + +func (self *SCloudaccount) InviteAzureUser(ctx context.Context, userCred mcclient.TokenCredential, domain string) (string, error) { + samlUsers, err := self.GetSamlusers() + if err != nil { + return "", errors.Wrapf(err, "GetSamlusers") + } + for i := range samlUsers { + if samlUsers[i].OwnerId == userCred.GetUserId() { + if len(samlUsers[i].Email) == 0 { + _, err := db.Update(&samlUsers[i], func() error { + samlUsers[i].Email = fmt.Sprintf("%s@%s", userCred.GetUserName(), domain) + return nil + }) + if err != nil { + return "", errors.Wrapf(err, "db.Update") + } + } + provider, err := self.GetProvider() + if err != nil { + return "", errors.Wrapf(err, "self.GetProvider") + } + conf := cloudprovider.SClouduserCreateConfig{ + Name: userCred.GetUserName(), + Email: samlUsers[i].Email, + UserType: "Guest", + } + iUser, err := provider.CreateIClouduser(&conf) + if err != nil { + return "", errors.Wrapf(err, "CreateIClouduser") + } + db.SetExternalId(&samlUsers[i], userCred, iUser.GetName()) + return iUser.GetInviteUrl(), nil + } + } + return "", fmt.Errorf("not found any saml user for %s", userCred.GetUserName()) +} diff --git a/pkg/cloudid/models/cloudgroupresource.go b/pkg/cloudid/models/cloudgroupresource.go index a7447e8a0d..8fae2b75c9 100644 --- a/pkg/cloudid/models/cloudgroupresource.go +++ b/pkg/cloudid/models/cloudgroupresource.go @@ -38,7 +38,7 @@ type SCloudgroupResourceBase struct { CloudgroupId string `width:"36" charset:"ascii" nullable:"false" list:"user" create:"required"` } -func (self *SCloudgroupJointsBase) GetCloudgroup() (*SCloudgroup, error) { +func (self *SCloudgroupResourceBase) GetCloudgroup() (*SCloudgroup, error) { group, err := CloudgroupManager.FetchById(self.CloudgroupId) if err != nil { return nil, errors.Wrap(err, "FetchById") diff --git a/pkg/cloudid/models/samluser.go b/pkg/cloudid/models/samluser.go index 3f433c5ba9..cfca1cf348 100644 --- a/pkg/cloudid/models/samluser.go +++ b/pkg/cloudid/models/samluser.go @@ -24,13 +24,17 @@ import ( api "yunion.io/x/onecloud/pkg/apis/cloudid" "yunion.io/x/onecloud/pkg/cloudcommon/db" "yunion.io/x/onecloud/pkg/cloudcommon/validators" + "yunion.io/x/onecloud/pkg/cloudid/options" "yunion.io/x/onecloud/pkg/httperrors" "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/mcclient/auth" "yunion.io/x/onecloud/pkg/util/stringutils2" ) type SSamluserManager struct { db.SStatusDomainLevelUserResourceBaseManager + db.SExternalizedResourceBaseManager + SCloudgroupResourceBaseManager SCloudaccountResourceBaseManager } @@ -51,8 +55,12 @@ func init() { type SSamluser struct { db.SStatusDomainLevelUserResourceBase + db.SExternalizedResourceBase SCloudgroupResourceBase SCloudaccountResourceBase + + // 邮箱地址 + Email string `width:"36" charset:"ascii" nullable:"true" list:"user" create:"domain_optional"` } func (manager *SSamluserManager) GetResourceCount() ([]db.SScopeResourceCount, error) { @@ -173,3 +181,38 @@ func (manager *SSamluserManager) FetchCustomizeColumns( } return rows } + +func (self *SSamluser) SyncAzureGroup() error { + group, err := self.GetCloudgroup() + if err != nil { + return errors.Wrapf(err, "GetCloudgroup") + } + account, err := self.GetCloudaccount() + if err != nil { + return errors.Wrapf(err, "GetCloudaccount") + } + cache, err := CloudgroupcacheManager.Register(group, account) + if err != nil { + return errors.Wrapf(err, "group cache Register") + } + if len(cache.ExternalId) == 0 { + s := auth.GetAdminSession(context.TODO(), options.Options.Region, "") + _, err = cache.GetOrCreateICloudgroup(context.TODO(), s.GetToken()) + if err != nil { + return errors.Wrapf(err, "GetOrCreateICloudgroup") + } + cache, err = CloudgroupcacheManager.Register(group, account) + if err != nil { + return errors.Wrapf(err, "group cache Register") + } + } + iGroup, err := cache.GetICloudgroup() + if err != nil { + return errors.Wrapf(err, "GetICloudgroup") + } + err = iGroup.AddUser(self.ExternalId) + if err != nil { + return errors.Wrapf(err, "iGroup.AddUser") + } + return nil +} diff --git a/pkg/cloudid/saml/load.go b/pkg/cloudid/saml/load.go index 02e9b5f4ac..855cc7e7f4 100644 --- a/pkg/cloudid/saml/load.go +++ b/pkg/cloudid/saml/load.go @@ -18,6 +18,7 @@ import ( _ "yunion.io/x/onecloud/pkg/cloudid/saml/providers/aliyun" _ "yunion.io/x/onecloud/pkg/cloudid/saml/providers/aws" _ "yunion.io/x/onecloud/pkg/cloudid/saml/providers/awscn" + _ "yunion.io/x/onecloud/pkg/cloudid/saml/providers/azure" _ "yunion.io/x/onecloud/pkg/cloudid/saml/providers/google" _ "yunion.io/x/onecloud/pkg/cloudid/saml/providers/huawei" _ "yunion.io/x/onecloud/pkg/cloudid/saml/providers/qcloud" diff --git a/pkg/cloudid/saml/providers/azure/doc.go b/pkg/cloudid/saml/providers/azure/doc.go new file mode 100644 index 0000000000..104872fd56 --- /dev/null +++ b/pkg/cloudid/saml/providers/azure/doc.go @@ -0,0 +1,15 @@ +// 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 azure // import "yunion.io/x/onecloud/pkg/cloudid/saml/providers/azure" diff --git a/pkg/cloudid/saml/providers/azure/driver.go b/pkg/cloudid/saml/providers/azure/driver.go new file mode 100644 index 0000000000..b1d07ae39d --- /dev/null +++ b/pkg/cloudid/saml/providers/azure/driver.go @@ -0,0 +1,131 @@ +// 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 azure + +import ( + "context" + "database/sql" + "fmt" + + "yunion.io/x/pkg/errors" + + api "yunion.io/x/onecloud/pkg/apis/compute" + "yunion.io/x/onecloud/pkg/cloudid/models" + "yunion.io/x/onecloud/pkg/httperrors" + "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/util/samlutils" + "yunion.io/x/onecloud/pkg/util/samlutils/idp" +) + +func (d *SAzureSAMLDriver) GetIdpInitiatedLoginData(ctx context.Context, userCred mcclient.TokenCredential, cloudAccountId string, sp *idp.SSAMLServiceProvider) (samlutils.SSAMLIdpInitiatedLoginData, error) { + data := samlutils.SSAMLIdpInitiatedLoginData{} + + _account, err := models.CloudaccountManager.FetchById(cloudAccountId) + if err != nil { + if errors.Cause(err) == sql.ErrNoRows { + return data, httperrors.NewResourceNotFoundError2("cloudaccount", cloudAccountId) + } + return data, httperrors.NewGeneralError(err) + } + account := _account.(*models.SCloudaccount) + if account.Provider != api.CLOUD_PROVIDER_AZURE { + return data, httperrors.NewClientError("cloudaccount %s is %s not %s", account.Id, account.Provider, api.CLOUD_PROVIDER_AWS) + } + if account.SAMLAuth.IsFalse() { + return data, httperrors.NewNotSupportedError("cloudaccount %s not open saml auth", account.Id) + } + + samlProvider, valid := account.IsSAMLProviderValid() + if !valid { + return data, httperrors.NewResourceNotReadyError("SAMLProvider for account %s not ready", account.Id) + } + + inviteUrl, err := account.InviteAzureUser(ctx, userCred, samlProvider.ExternalId) + if err != nil { + return data, httperrors.NewGeneralError(errors.Wrapf(err, "InviteAzureUser")) + } + + data.Form = fmt.Sprintf(` + + + + + waiting... + + + waiting... + + `, inviteUrl) + return data, nil +} + +func (d *SAzureSAMLDriver) GetSpInitiatedLoginData(ctx context.Context, userCred mcclient.TokenCredential, cloudAccountId string, sp *idp.SSAMLServiceProvider) (samlutils.SSAMLSpInitiatedLoginData, error) { + data := samlutils.SSAMLSpInitiatedLoginData{} + + _account, err := models.CloudaccountManager.FetchById(cloudAccountId) + if err != nil { + if errors.Cause(err) == sql.ErrNoRows { + return data, httperrors.NewResourceNotFoundError2("cloudaccount", cloudAccountId) + } + return data, httperrors.NewGeneralError(err) + } + account := _account.(*models.SCloudaccount) + if account.Provider != api.CLOUD_PROVIDER_AZURE { + return data, httperrors.NewClientError("cloudaccount %s is %s not %s", account.Id, account.Provider, api.CLOUD_PROVIDER_AWS) + } + if account.SAMLAuth.IsFalse() { + return data, httperrors.NewNotSupportedError("cloudaccount %s not open saml auth", account.Id) + } + + samlUsers, err := account.GetSamlusers() + if err != nil { + return data, httperrors.NewGeneralError(errors.Wrapf(err, "GetSamlusers")) + } + + for i := range samlUsers { + if samlUsers[i].OwnerId == userCred.GetUserId() && samlUsers[i].Email == sp.Username { + + err := samlUsers[i].SyncAzureGroup() + if err != nil { + return data, errors.Wrapf(err, "SyncAzureGroup") + } + + data.NameId = sp.Username + + data.NameIdFormat = samlutils.NAME_ID_FORMAT_PERSISTENT + data.AudienceRestriction = sp.GetEntityId() + for _, v := range []struct { + name string + friendlyName string + value string + }{ + { + name: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", + value: data.NameId, + }, + } { + data.Attributes = append(data.Attributes, samlutils.SSAMLResponseAttribute{ + Name: v.name, + FriendlyName: v.friendlyName, + Values: []string{v.value}, + }) + } + return data, nil + + } + } + + return data, httperrors.NewResourceNotFoundError("not found any saml user for %s -> %s", userCred.GetUserName(), sp.Username) +} diff --git a/pkg/cloudid/saml/providers/azure/factory.go b/pkg/cloudid/saml/providers/azure/factory.go new file mode 100644 index 0000000000..3dc3cffa58 --- /dev/null +++ b/pkg/cloudid/saml/providers/azure/factory.go @@ -0,0 +1,38 @@ +// 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 azure + +import ( + "yunion.io/x/onecloud/pkg/cloudid/models" + "yunion.io/x/onecloud/pkg/cloudprovider" +) + +type SAzureSAMLDriver struct{} + +func (d *SAzureSAMLDriver) GetEntityID() string { + return cloudprovider.SAML_ENTITY_ID_AZURE +} + +func (d *SAzureSAMLDriver) GetMetadataFilename() string { + return "azure.xml" +} + +func (d *SAzureSAMLDriver) GetMetadataUrl() string { + return "https://nexus.microsoftonline-p.com/federationmetadata/saml20/federationmetadata.xml" +} + +func init() { + models.Register(&SAzureSAMLDriver{}) +} diff --git a/pkg/cloudprovider/clouduser.go b/pkg/cloudprovider/clouduser.go index 1a1844a1d1..568645bc58 100644 --- a/pkg/cloudprovider/clouduser.go +++ b/pkg/cloudprovider/clouduser.go @@ -24,6 +24,7 @@ type SClouduserCreateConfig struct { Email string MobilePhone string ExternalPolicyIds []string + UserType string } type SCloudpolicyPermission struct { diff --git a/pkg/cloudprovider/resources.go b/pkg/cloudprovider/resources.go index 896e94069c..2c8fccae55 100644 --- a/pkg/cloudprovider/resources.go +++ b/pkg/cloudprovider/resources.go @@ -1072,6 +1072,9 @@ type IClouduser interface { GetGlobalId() string GetName() string + GetEmailAddr() string + GetInviteUrl() string + GetICloudgroups() ([]ICloudgroup, error) GetISystemCloudpolicies() ([]ICloudpolicy, error) diff --git a/pkg/cloudprovider/saml.go b/pkg/cloudprovider/saml.go index 469c12f119..eaa95c1532 100644 --- a/pkg/cloudprovider/saml.go +++ b/pkg/cloudprovider/saml.go @@ -23,6 +23,7 @@ const ( SAML_ENTITY_ID_QCLOUD = "cloud.tencent.com" SAML_ENTITY_ID_HUAWEI_CLOUD = "https://auth.huaweicloud.com/" SAML_ENTITY_ID_GOOGLE = "google.com" + SAML_ENTITY_ID_AZURE = "urn:federation:MicrosoftOnline" ) type SAMLProviderCreateOptions struct { diff --git a/pkg/mcclient/options/cloudid/cloudgroup.go b/pkg/mcclient/options/cloudid/cloudgroup.go index ecef8754ed..3815f34918 100644 --- a/pkg/mcclient/options/cloudid/cloudgroup.go +++ b/pkg/mcclient/options/cloudid/cloudgroup.go @@ -34,7 +34,7 @@ func (opts *CloudgroupListOptions) Params() (jsonutils.JSONObject, error) { type CloudgroupCreateOptions struct { NAME string `json:"name"` - PROVIDER string `json:"provider" choices:"Google|Aliyun|Aws|Huawei|Qcloud"` + PROVIDER string `json:"provider" choices:"Google|Aliyun|Aws|Huawei|Qcloud|Azure"` CloudpolicyIds []string `json:"cloudpolicy_ids"` Desc string `json:"description"` } diff --git a/pkg/multicloud/aliyun/ram_user.go b/pkg/multicloud/aliyun/ram_user.go index f597702433..2ebba89d8e 100644 --- a/pkg/multicloud/aliyun/ram_user.go +++ b/pkg/multicloud/aliyun/ram_user.go @@ -61,6 +61,14 @@ func (user *SUser) GetName() string { return user.UserName } +func (user *SUser) GetEmailAddr() string { + return user.Email +} + +func (user *SUser) GetInviteUrl() string { + return "" +} + func (user *SUser) Delete() error { groups, err := user.client.ListGroupsForUser(user.UserName) if err != nil { diff --git a/pkg/multicloud/apsara/ram_user.go b/pkg/multicloud/apsara/ram_user.go index 1edac509b1..df016b2015 100644 --- a/pkg/multicloud/apsara/ram_user.go +++ b/pkg/multicloud/apsara/ram_user.go @@ -46,6 +46,14 @@ type SUser struct { UserName string } +func (user *SUser) GetEmailAddr() string { + return "" +} + +func (user *SUser) GetInviteUrl() string { + return "" +} + func (user *SUser) GetGlobalId() string { if len(user.UserId) > 0 { return user.UserId diff --git a/pkg/multicloud/aws/iam_user.go b/pkg/multicloud/aws/iam_user.go index b5bb1353d8..aecd2bf899 100644 --- a/pkg/multicloud/aws/iam_user.go +++ b/pkg/multicloud/aws/iam_user.go @@ -41,6 +41,14 @@ type SUser struct { PasswordLastUsed time.Time `xml:"PasswordLastUsed"` } +func (user *SUser) GetEmailAddr() string { + return "" +} + +func (user *SUser) GetInviteUrl() string { + return "" +} + func (user *SUser) AttachSystemPolicy(policyArn string) error { return user.client.AttachUserPolicy(user.UserName, user.client.getIamArn(policyArn)) } diff --git a/pkg/multicloud/azure/azure.go b/pkg/multicloud/azure/azure.go index 038265c47d..3f24f70bcc 100644 --- a/pkg/multicloud/azure/azure.go +++ b/pkg/multicloud/azure/azure.go @@ -27,6 +27,7 @@ import ( azureenv "github.com/Azure/go-autorest/autorest/azure" "github.com/Azure/go-autorest/autorest/azure/auth" "github.com/pkg/errors" + "golang.org/x/oauth2/clientcredentials" "yunion.io/x/jsonutils" "yunion.io/x/log" @@ -658,8 +659,8 @@ func (ae *AzureResponseError) ParseErrorFromJsonResponse(statusCode int, body js } func _jsonRequest(client *autorest.Client, method, domain, path string, body jsonutils.JSONObject, params url.Values, debug bool) (jsonutils.JSONObject, error) { - url := fmt.Sprintf("%s/%s?%s", strings.TrimSuffix(domain, "/"), strings.TrimPrefix(path, "/"), params.Encode()) - req := httputils.NewJsonRequest(httputils.THttpMethod(method), url, body) + uri := fmt.Sprintf("%s/%s?%s", strings.TrimSuffix(domain, "/"), strings.TrimPrefix(path, "/"), params.Encode()) + req := httputils.NewJsonRequest(httputils.THttpMethod(method), uri, body) ae := AzureResponseError{} cli := httputils.NewJsonClient(client) header, body, err := cli.Send(context.TODO(), req, &ae, debug) @@ -678,7 +679,16 @@ func _jsonRequest(client *autorest.Client, method, domain, path string, body jso location := locationFunc(header) if len(location) > 0 && (body == nil || body.IsZero() || !body.Contains("id")) { err = cloudprovider.Wait(time.Second*10, time.Minute*30, func() (bool, error) { - req := httputils.NewJsonRequest(httputils.GET, location, nil) + locationUrl, err := url.Parse(location) + if err != nil { + return false, errors.Wrapf(err, "url.Parse(%s)", location) + } + if len(locationUrl.Query().Get("api-version")) == 0 { + q, _ := url.ParseQuery(locationUrl.RawQuery) + q.Set("api-version", params.Get("api-version")) + locationUrl.RawQuery = q.Encode() + } + req := httputils.NewJsonRequest(httputils.GET, locationUrl.String(), nil) lae := AzureResponseError{} _header, _body, _err := cli.Send(context.TODO(), req, &lae, debug) if _err != nil { @@ -724,7 +734,7 @@ func _jsonRequest(client *autorest.Client, method, domain, path string, body jso return false, nil }) if err != nil { - return nil, errors.Wrapf(err, "time out for waiting %s %s", method, url) + return nil, errors.Wrapf(err, "time out for waiting %s %s", method, uri) } } return body, nil @@ -922,6 +932,7 @@ func (self *SAzureClient) GetCapabilities() []string { // cloudprovider.CLOUD_CAPABILITY_CACHE, cloudprovider.CLOUD_CAPABILITY_EVENT, cloudprovider.CLOUD_CAPABILITY_CLOUDID, + cloudprovider.CLOUD_CAPABILITY_SAML_AUTH, } return caps } @@ -956,3 +967,40 @@ func (self *SAzureClient) SetTags(resourceId string, tags map[string]string) (js input.Properties.Tags = tags return self.put(path, jsonutils.Marshal(input)) } + +func (self *SAzureClient) msGraphClient() *http.Client { + conf := clientcredentials.Config{ + ClientID: self.clientId, + ClientSecret: self.clientSecret, + + TokenURL: fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/v2.0/token", self.tenantId), + Scopes: []string{"https://graph.microsoft.com/.default"}, + } + return conf.Client(context.TODO()) +} + +func (self *SAzureClient) ListGraphUsers() ([]SClouduser, error) { + resp, err := self.msGraphRequest("GET", "users", nil) + if err != nil { + return nil, errors.Wrapf(err, "msGraphRequest.users") + } + users := []SClouduser{} + err = resp.Unmarshal(&users, "value") + if err != nil { + return nil, errors.Wrapf(err, "resp.Unmarshal") + } + return users, nil +} + +func (self *SAzureClient) msGraphRequest(method string, resource string, body jsonutils.JSONObject) (jsonutils.JSONObject, error) { + client := self.msGraphClient() + url := fmt.Sprintf("https://graph.microsoft.com/v1.0/%s", resource) + req := httputils.NewJsonRequest(httputils.THttpMethod(method), url, body) + ae := AzureResponseError{} + cli := httputils.NewJsonClient(client) + _, body, err := cli.Send(context.TODO(), req, &ae, self.debug) + if err != nil { + return nil, err + } + return body, nil +} diff --git a/pkg/multicloud/azure/cloudpolicy.go b/pkg/multicloud/azure/cloudpolicy.go index b933453806..61352b1b76 100644 --- a/pkg/multicloud/azure/cloudpolicy.go +++ b/pkg/multicloud/azure/cloudpolicy.go @@ -134,9 +134,8 @@ func (cli *SAzureClient) AssignPolicy(objectId, roleName, subscriptionId string) "principalId": objectId, }, } - subscriptionIds := []string{subscriptionId} + subscriptionIds := []string{} if len(subscriptionId) == 0 { - subscriptionIds = []string{} for _, subscription := range cli.subscriptions { subscriptionIds = append(subscriptionIds, subscription.SubscriptionId) } diff --git a/pkg/multicloud/azure/clouduser.go b/pkg/multicloud/azure/clouduser.go index 7fa60062a8..c7b60c6243 100644 --- a/pkg/multicloud/azure/clouduser.go +++ b/pkg/multicloud/azure/clouduser.go @@ -80,6 +80,8 @@ type SClouduser struct { UserState string UserStateChangedOn string UserType string + + inviteRedeemUrl string } func (user *SClouduser) GetName() string { @@ -90,6 +92,14 @@ func (user *SClouduser) GetGlobalId() string { return user.ObjectId } +func (user *SClouduser) GetEmailAddr() string { + return user.Mail +} + +func (user *SClouduser) GetInviteUrl() string { + return user.inviteRedeemUrl +} + func (user *SClouduser) GetISystemCloudpolicies() ([]cloudprovider.ICloudpolicy, error) { policies, err := user.client.GetCloudpolicies(user.ObjectId) if err != nil { @@ -250,6 +260,9 @@ func (self *SAzureClient) GetIClouduserByName(name string) (cloudprovider.ICloud } func (self *SAzureClient) CreateIClouduser(conf *cloudprovider.SClouduserCreateConfig) (cloudprovider.IClouduser, error) { + if conf.UserType == "Guest" { + return self.InviteUser(conf.Email) + } return self.CreateClouduser(conf.Name, conf.Password) } diff --git a/pkg/multicloud/azure/provider/provider.go b/pkg/multicloud/azure/provider/provider.go index 4837e0392d..b867501429 100644 --- a/pkg/multicloud/azure/provider/provider.go +++ b/pkg/multicloud/azure/provider/provider.go @@ -54,6 +54,10 @@ func (self *SAzureProviderFactory) IsCloudeventRegional() bool { return false } +func (self *SAzureProviderFactory) IsSupportSAMLAuth() bool { + return true +} + func (self *SAzureProviderFactory) ValidateChangeBandwidth(instanceId string, bandwidth int64) error { return fmt.Errorf("Changing %s bandwidth is not supported", azure.CLOUD_PROVIDER_AZURE) } @@ -271,3 +275,12 @@ func (self *SAzureProvider) GetICustomCloudpolicies() ([]cloudprovider.ICloudpol func (self *SAzureProvider) CreateSubscription(input cloudprovider.SubscriptionCreateInput) error { return self.client.CreateSubscription(input.Name, input.EnrollmentAccountId, input.OfferType) } + +// fake func +func (self *SAzureProvider) CreateICloudSAMLProvider(opts *cloudprovider.SAMLProviderCreateOptions) (cloudprovider.ICloudSAMLProvider, error) { + return self.client.CreateSAMLProvider(opts) +} + +func (self *SAzureProvider) GetSamlEntityId() string { + return cloudprovider.SAML_ENTITY_ID_AZURE +} diff --git a/pkg/multicloud/azure/saml_provider.go b/pkg/multicloud/azure/saml_provider.go new file mode 100644 index 0000000000..d9b4debb2b --- /dev/null +++ b/pkg/multicloud/azure/saml_provider.go @@ -0,0 +1,136 @@ +// 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 azure + +import ( + "fmt" + "strings" + "time" + + "yunion.io/x/jsonutils" + "yunion.io/x/pkg/errors" + + "yunion.io/x/onecloud/pkg/apis/cloudid" + api "yunion.io/x/onecloud/pkg/apis/cloudid" + compute_api "yunion.io/x/onecloud/pkg/apis/compute" + "yunion.io/x/onecloud/pkg/cloudid/options" + "yunion.io/x/onecloud/pkg/cloudprovider" + "yunion.io/x/onecloud/pkg/multicloud" + "yunion.io/x/onecloud/pkg/util/httputils" + "yunion.io/x/onecloud/pkg/util/samlutils" +) + +type SAMLProvider struct { + multicloud.SResourceBase + client *SAzureClient + + Name string + Metadata samlutils.EntityDescriptor +} + +func (self *SAMLProvider) Delete() error { + return nil +} + +func (self *SAMLProvider) GetGlobalId() string { + return strings.TrimPrefix(options.Options.ApiServer, "https://") +} + +func (self *SAMLProvider) GetId() string { + return options.Options.ApiServer +} + +func (self *SAMLProvider) GetName() string { + return self.Name +} + +func (self *SAMLProvider) GetStatus() string { + return api.SAML_PROVIDER_STATUS_AVAILABLE +} + +func (self *SAMLProvider) UpdateMetadata(metadata samlutils.EntityDescriptor) error { + return nil +} + +func (self *SAMLProvider) GetMetadataDocument() (*samlutils.EntityDescriptor, error) { + return &self.Metadata, nil +} + +func (self *SAMLProvider) GetAuthUrl() string { + input := samlutils.SIdpInitiatedLoginInput{ + EntityID: cloudprovider.SAML_ENTITY_ID_AZURE, + IdpId: self.client.cpcfg.AccountId, + } + if self.client.GetAccessEnv() != compute_api.CLOUD_ACCESS_ENV_AZURE_GLOBAL { + return "" + } + return httputils.JoinPath(options.Options.ApiServer, cloudid.SAML_IDP_PREFIX, fmt.Sprintf("sso?%s", jsonutils.Marshal(input).QueryString())) +} + +func (self *SAzureClient) ListSAMLProviders() ([]SAMLProvider, error) { + _, err := self.msGraphRequest("GET", "identityProviders", nil) + if err != nil { + return nil, err + } + return []SAMLProvider{}, nil +} + +func (self *SAzureClient) InviteUser(email string) (*SClouduser, error) { + body := jsonutils.Marshal(map[string]string{ + "invitedUserEmailAddress": email, + "inviteRedirectUrl": fmt.Sprintf("https://portal.azure.com/%s", self.tenantId), + }) + resp, err := self.msGraphRequest("POST", "invitations", body) + if err != nil { + return nil, errors.Wrapf(err, "msGraphRequest.invitations") + } + inviteUrl, _ := resp.GetString("inviteRedeemUrl") + err = cloudprovider.Wait(time.Second*2, time.Minute, func() (bool, error) { + users, err := self.ListGraphUsers() + if err != nil { + return false, errors.Wrapf(err, "GetCloudusers") + } + for i := range users { + users[i].inviteRedeemUrl = inviteUrl + if users[i].GetEmailAddr() == email { + return true, nil + } + } + return false, nil + }) + if err != nil { + return nil, errors.Wrapf(cloudprovider.ErrNotFound, "after invite %s", email) + } + users, err := self.ListGraphUsers() + if err != nil { + return nil, errors.Wrapf(err, "GetCloudusers") + } + for i := range users { + users[i].inviteRedeemUrl = inviteUrl + if users[i].GetEmailAddr() == email { + return &users[i], nil + } + } + + return nil, errors.Wrapf(cloudprovider.ErrNotFound, "after invite %s", email) +} + +func (self *SAzureClient) CreateSAMLProvider(opts *cloudprovider.SAMLProviderCreateOptions) (*SAMLProvider, error) { + return &SAMLProvider{ + client: self, + Name: opts.Name, + Metadata: opts.Metadata, + }, nil +} diff --git a/pkg/multicloud/azure/shell/clouduser.go b/pkg/multicloud/azure/shell/clouduser.go index aad7761513..eac9fa8400 100644 --- a/pkg/multicloud/azure/shell/clouduser.go +++ b/pkg/multicloud/azure/shell/clouduser.go @@ -84,4 +84,16 @@ func init() { return nil }) + type GroupUserList struct { + } + + shellutils.R(&GroupUserList{}, "graph-user-list", "List graph users", func(cli *azure.SRegion, args *GroupUserList) error { + users, err := cli.GetClient().ListGraphUsers() + if err != nil { + return err + } + printObject(users) + return nil + }) + } diff --git a/pkg/multicloud/azure/shell/saml_provider.go b/pkg/multicloud/azure/shell/saml_provider.go new file mode 100644 index 0000000000..58073c6d5b --- /dev/null +++ b/pkg/multicloud/azure/shell/saml_provider.go @@ -0,0 +1,50 @@ +// 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 shell + +import ( + "fmt" + + "yunion.io/x/onecloud/pkg/multicloud/azure" + "yunion.io/x/onecloud/pkg/util/shellutils" +) + +func init() { + type SAMLProviderListOptions struct { + } + shellutils.R(&SAMLProviderListOptions{}, "saml-provider-list", "List regions", func(cli *azure.SRegion, args *SAMLProviderListOptions) error { + sps, err := cli.GetClient().ListSAMLProviders() + if err != nil { + return err + } + printList(sps, 0, 0, 0, nil) + return nil + }) + + type SInvitateUser struct { + EMAIL string + } + + shellutils.R(&SInvitateUser{}, "invite-user", "Invitate user", func(cli *azure.SRegion, args *SInvitateUser) error { + user, err := cli.GetClient().InviteUser(args.EMAIL) + if err != nil { + return err + } + printObject(user) + fmt.Println("invite url: ", user.GetInviteUrl()) + return nil + }) + +} diff --git a/pkg/multicloud/google/iampolicy.go b/pkg/multicloud/google/iampolicy.go index 81f9af3f20..f8356160d6 100644 --- a/pkg/multicloud/google/iampolicy.go +++ b/pkg/multicloud/google/iampolicy.go @@ -140,6 +140,14 @@ type SClouduser struct { Roles []string } +func (self *SClouduser) GetEmailAddr() string { + return "" +} + +func (self *SClouduser) GetInviteUrl() string { + return "" +} + func (self *SGoogleClient) GetISystemCloudpolicies() ([]cloudprovider.ICloudpolicy, error) { roles, err := self.GetRoles("") if err != nil { diff --git a/pkg/multicloud/huawei/clouduser.go b/pkg/multicloud/huawei/clouduser.go index 4d29648a30..0221b87558 100644 --- a/pkg/multicloud/huawei/clouduser.go +++ b/pkg/multicloud/huawei/clouduser.go @@ -51,6 +51,14 @@ func (user *SClouduser) GetName() string { return user.Name } +func (user *SClouduser) GetEmailAddr() string { + return "" +} + +func (user *SClouduser) GetInviteUrl() string { + return "" +} + func (user *SClouduser) GetISystemCloudpolicies() ([]cloudprovider.ICloudpolicy, error) { return []cloudprovider.ICloudpolicy{}, nil } diff --git a/pkg/multicloud/qcloud/cam_user.go b/pkg/multicloud/qcloud/cam_user.go index 13f520705d..13275ea46b 100644 --- a/pkg/multicloud/qcloud/cam_user.go +++ b/pkg/multicloud/qcloud/cam_user.go @@ -39,6 +39,14 @@ func (user *SUser) GetGlobalId() string { return fmt.Sprintf("%d", user.Uin) } +func (self *SUser) GetEmailAddr() string { + return self.Email +} + +func (self *SUser) GetInviteUrl() string { + return "" +} + func (user *SUser) GetISystemCloudpolicies() ([]cloudprovider.ICloudpolicy, error) { policies := []SPolicy{} offset := 1 diff --git a/pkg/util/samlutils/demo/service.go b/pkg/util/samlutils/demo/service.go index 3296aab511..951a1e027a 100644 --- a/pkg/util/samlutils/demo/service.go +++ b/pkg/util/samlutils/demo/service.go @@ -194,6 +194,27 @@ func prepareServer() error { Values: []string{v.value}, }) } + case "urn:federation:MicrosoftOnline": + data.NameId = sp.Username + data.NameIdFormat = samlutils.NAME_ID_FORMAT_PERSISTENT + data.AudienceRestriction = sp.GetEntityId() + for _, v := range []struct { + name string + friendlyName string + value string + }{ + { + name: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", + value: data.NameId, + }, + } { + data.Attributes = append(data.Attributes, samlutils.SSAMLResponseAttribute{ + Name: v.name, + FriendlyName: v.friendlyName, + Values: []string{v.value}, + }) + } + return data, nil case "google.com/a/yunion-hk.com": data.NameId = "qiujian" data.NameIdFormat = samlutils.NAME_ID_FORMAT_TRANSIENT @@ -406,6 +427,10 @@ func prepareServer() error { name: "Google cloud SSO", url: "https://www.google.com/a/yunion-hk.com/ServiceLogin?continue=https://console.cloud.google.com", }, + { + name: "Azure cloud SSO", + url: "https://login.microsoftonline.com/redeem?rd=https%3a%2f%2finvitations.microsoft.com%2fredeem%2f%3ftenant%3d17493ddf-fa90-4f95-8576-5df011c126e5%26user%3d3bc1c055-aa14-4795-aef0-5970b00d03c7%26ticket%3d0GDu%252bZ7nLbg01rYL5u%252b401%252bOLyZjxPewSBJIAZZ7E0U%253d%26ver%3d2.0", + }, } { htmlBuf.WriteString(fmt.Sprintf(`
  • %s (SP-Initiated)
  • `, v.url, v.name)) } diff --git a/pkg/util/samlutils/idp/idp.go b/pkg/util/samlutils/idp/idp.go index e9751e3b32..0d4156b7f5 100644 --- a/pkg/util/samlutils/idp/idp.go +++ b/pkg/util/samlutils/idp/idp.go @@ -77,6 +77,7 @@ func (idp *SSAMLIdpInstance) AddHandlers(app *appsrv.Application, prefix string, if middleware != nil { handler = middleware(handler) } + app.AddHandler("POST", idp.redirectLoginPath, handler) app.AddHandler("GET", idp.redirectLoginPath, handler) handler = idp.redirectLogoutHandler if middleware != nil { @@ -224,16 +225,17 @@ func (idp *SSAMLIdpInstance) processLoginRequest(ctx context.Context, idpId stri return "", errors.Wrapf(httperrors.ErrInputParameter, "Destination not match: get %s want %s", authReq.Destination, idp.getRedirectLoginUrl(idpId)) } - if authReq.AssertionConsumerServiceURL != sp.GetPostAssertionConsumerServiceUrl() { + if len(authReq.AssertionConsumerServiceURL) > 0 && authReq.AssertionConsumerServiceURL != sp.GetPostAssertionConsumerServiceUrl() { return "", errors.Wrapf(httperrors.ErrInputParameter, "AssertionConsumerServiceURL not match: get %s want %s", authReq.AssertionConsumerServiceURL, sp.GetPostAssertionConsumerServiceUrl()) } + sp.Username = input.Username resp, err := idp.getLoginResponse(ctx, authReq, idpId, sp) if err != nil { return "", errors.Wrap(err, "getLoginResponse") } - form, err := idp.samlResponse2Form(authReq.AssertionConsumerServiceURL, resp, input.RelayState) + form, err := idp.samlResponse2Form(sp.GetPostAssertionConsumerServiceUrl(), resp, input.RelayState) if err != nil { return "", errors.Wrap(err, "samlResponse2Form") } @@ -246,7 +248,6 @@ func (idp *SSAMLIdpInstance) samlResponse2Form(url string, resp *samlutils.Respo if err != nil { return "", errors.Wrap(err, "xml.Marshal") } - signed, err := idp.saml.SignXML(string(respXml)) if err != nil { return "", errors.Wrap(err, "saml.SignXML") @@ -287,7 +288,7 @@ func (idp *SSAMLIdpInstance) getLoginResponse(ctx context.Context, req samlutils IssuerEntityId: idp.saml.GetEntityId(), RequestID: req.ID, RequestEntityId: req.Issuer.Issuer, - AssertionConsumerServiceURL: req.AssertionConsumerServiceURL, + AssertionConsumerServiceURL: sp.GetPostAssertionConsumerServiceUrl(), SSAMLSpInitiatedLoginData: data, } resp := samlutils.NewResponse(input) @@ -303,6 +304,9 @@ func (idp *SSAMLIdpInstance) processIdpInitiatedLogin(ctx context.Context, input if err != nil { return "", errors.Wrap(err, "idp.onIdpInitiatedLogin") } + if len(data.Form) > 0 { + return data.Form, nil + } respInput := samlutils.SSAMLResponseInput{ IssuerCertString: idp.saml.GetCertString(), IssuerEntityId: idp.saml.GetEntityId(), diff --git a/pkg/util/samlutils/idp/sp.go b/pkg/util/samlutils/idp/sp.go index 0d09d3775b..faf01120c2 100644 --- a/pkg/util/samlutils/idp/sp.go +++ b/pkg/util/samlutils/idp/sp.go @@ -23,6 +23,8 @@ import ( type SSAMLServiceProvider struct { desc samlutils.EntityDescriptor + + Username string } func (sp *SSAMLServiceProvider) GetEntityId() string { diff --git a/pkg/util/samlutils/response.go b/pkg/util/samlutils/response.go index b7dc39c3cd..235ce16012 100644 --- a/pkg/util/samlutils/response.go +++ b/pkg/util/samlutils/response.go @@ -36,6 +36,8 @@ type SSAMLSpInitiatedLoginData struct { AudienceRestriction string Attributes []SSAMLResponseAttribute + + Form string } type SSAMLIdpInitiatedLoginData struct { @@ -59,7 +61,7 @@ type SSAMLResponseInput struct { func NewResponse(input SSAMLResponseInput) Response { // since := timeutils.IsoTime(time.Now().UTC().Add(-time.Minute * 60 * 24)) - until := timeutils.IsoTime(time.Now().UTC().Add(time.Minute * 60 * 24)) + until := timeutils.IsoTime(time.Now().UTC().Add(time.Minute * 5)) respId := GenerateSAMLId() assertId := GenerateSAMLId() @@ -251,6 +253,7 @@ func NewResponse(input SSAMLResponseInput) Response { Space: XMLNS_ASSERT, Local: "Conditions", }, + NotBefore: &now, NotOnOrAfter: until, AudienceRestrictions: []AudienceRestriction{}, }, diff --git a/pkg/util/samlutils/types.go b/pkg/util/samlutils/types.go index f407406cf6..ce036ee500 100644 --- a/pkg/util/samlutils/types.go +++ b/pkg/util/samlutils/types.go @@ -250,6 +250,8 @@ type SIdpRedirectLoginInput struct { RelayState string `json:"RelayState,ignoreempty"` SigAlg string `json:"SigAlg,ignoreempty"` Signature string `json:"Signature,ignoreempty"` + + Username string `json:"username,ignoreempty"` } type SIdpInitiatedLoginInput struct { diff --git a/pkg/util/samlutils/util.go b/pkg/util/samlutils/util.go index a07b710283..0e934ee0d3 100644 --- a/pkg/util/samlutils/util.go +++ b/pkg/util/samlutils/util.go @@ -25,6 +25,7 @@ import ( "github.com/ma314smith/signedxml" + "yunion.io/x/log" "yunion.io/x/pkg/errors" "yunion.io/x/pkg/utils" ) @@ -72,11 +73,15 @@ func SAMLDecode(input string) ([]byte, error) { if err != nil { return nil, errors.Wrap(err, "base64.StdEncoding.DecodeString") } - plainText, err := decompress(reqBytes) - if err != nil { - return nil, errors.Wrap(err, "decompress") - } - return plainText, nil + return func() []byte { + // Azure no need to decompress + plainText, err := decompress(reqBytes) + if err != nil { + log.Warningf("decompress %s error: %v", string(reqBytes), err) + return reqBytes + } + return plainText + }(), nil } func SAMLEncode(input []byte) (string, error) { diff --git a/vendor/golang.org/x/oauth2/clientcredentials/clientcredentials.go b/vendor/golang.org/x/oauth2/clientcredentials/clientcredentials.go new file mode 100644 index 0000000000..7a0b9ed102 --- /dev/null +++ b/vendor/golang.org/x/oauth2/clientcredentials/clientcredentials.go @@ -0,0 +1,120 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package clientcredentials implements the OAuth2.0 "client credentials" token flow, +// also known as the "two-legged OAuth 2.0". +// +// This should be used when the client is acting on its own behalf or when the client +// is the resource owner. It may also be used when requesting access to protected +// resources based on an authorization previously arranged with the authorization +// server. +// +// See https://tools.ietf.org/html/rfc6749#section-4.4 +package clientcredentials // import "golang.org/x/oauth2/clientcredentials" + +import ( + "context" + "fmt" + "net/http" + "net/url" + "strings" + + "golang.org/x/oauth2" + "golang.org/x/oauth2/internal" +) + +// Config describes a 2-legged OAuth2 flow, with both the +// client application information and the server's endpoint URLs. +type Config struct { + // ClientID is the application's ID. + ClientID string + + // ClientSecret is the application's secret. + ClientSecret string + + // TokenURL is the resource server's token endpoint + // URL. This is a constant specific to each server. + TokenURL string + + // Scope specifies optional requested permissions. + Scopes []string + + // EndpointParams specifies additional parameters for requests to the token endpoint. + EndpointParams url.Values + + // AuthStyle optionally specifies how the endpoint wants the + // client ID & client secret sent. The zero value means to + // auto-detect. + AuthStyle oauth2.AuthStyle +} + +// Token uses client credentials to retrieve a token. +// +// The provided context optionally controls which HTTP client is used. See the oauth2.HTTPClient variable. +func (c *Config) Token(ctx context.Context) (*oauth2.Token, error) { + return c.TokenSource(ctx).Token() +} + +// Client returns an HTTP client using the provided token. +// The token will auto-refresh as necessary. +// +// The provided context optionally controls which HTTP client +// is returned. See the oauth2.HTTPClient variable. +// +// The returned Client and its Transport should not be modified. +func (c *Config) Client(ctx context.Context) *http.Client { + return oauth2.NewClient(ctx, c.TokenSource(ctx)) +} + +// TokenSource returns a TokenSource that returns t until t expires, +// automatically refreshing it as necessary using the provided context and the +// client ID and client secret. +// +// Most users will use Config.Client instead. +func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource { + source := &tokenSource{ + ctx: ctx, + conf: c, + } + return oauth2.ReuseTokenSource(nil, source) +} + +type tokenSource struct { + ctx context.Context + conf *Config +} + +// Token refreshes the token by using a new client credentials request. +// tokens received this way do not include a refresh token +func (c *tokenSource) Token() (*oauth2.Token, error) { + v := url.Values{ + "grant_type": {"client_credentials"}, + } + if len(c.conf.Scopes) > 0 { + v.Set("scope", strings.Join(c.conf.Scopes, " ")) + } + for k, p := range c.conf.EndpointParams { + // Allow grant_type to be overridden to allow interoperability with + // non-compliant implementations. + if _, ok := v[k]; ok && k != "grant_type" { + return nil, fmt.Errorf("oauth2: cannot overwrite parameter %q", k) + } + v[k] = p + } + + tk, err := internal.RetrieveToken(c.ctx, c.conf.ClientID, c.conf.ClientSecret, c.conf.TokenURL, v, internal.AuthStyle(c.conf.AuthStyle)) + if err != nil { + if rErr, ok := err.(*internal.RetrieveError); ok { + return nil, (*oauth2.RetrieveError)(rErr) + } + return nil, err + } + t := &oauth2.Token{ + AccessToken: tk.AccessToken, + TokenType: tk.TokenType, + RefreshToken: tk.RefreshToken, + Expiry: tk.Expiry, + } + return t.WithExtra(tk.Raw), nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 95ade2a227..74ce7c7dee 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -725,6 +725,7 @@ golang.org/x/net/publicsuffix golang.org/x/net/trace # golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6 golang.org/x/oauth2 +golang.org/x/oauth2/clientcredentials golang.org/x/oauth2/google golang.org/x/oauth2/internal golang.org/x/oauth2/jws @@ -1105,7 +1106,7 @@ k8s.io/utils/integer sigs.k8s.io/structured-merge-diff/v4/value # sigs.k8s.io/yaml v1.2.0 sigs.k8s.io/yaml -# yunion.io/x/executor v0.0.0-20201201131200-44fa553abd9e +# yunion.io/x/executor v0.0.0-20201231064744-df32f32165a9 yunion.io/x/executor/apis yunion.io/x/executor/client yunion.io/x/executor/server @@ -1118,7 +1119,7 @@ yunion.io/x/log/hooks yunion.io/x/ovsdb/cli_util yunion.io/x/ovsdb/schema/ovn_nb yunion.io/x/ovsdb/types -# yunion.io/x/pkg v0.0.0-20201123083159-ca3aea986ff2 +# yunion.io/x/pkg v0.0.0-20210109071527-7e72daf56747 yunion.io/x/pkg/errors yunion.io/x/pkg/gotypes yunion.io/x/pkg/prettytable diff --git a/vendor/yunion.io/x/pkg/util/compare/compare.go b/vendor/yunion.io/x/pkg/util/compare/compare.go index 4c3d62b1f6..fb64a78af8 100644 --- a/vendor/yunion.io/x/pkg/util/compare/compare.go +++ b/vendor/yunion.io/x/pkg/util/compare/compare.go @@ -19,6 +19,8 @@ import ( "reflect" "sort" "strings" + + "yunion.io/x/pkg/errors" ) type valueElement struct { @@ -79,6 +81,19 @@ func CompareSets(dbSet interface{}, extSet interface{}, removed interface{}, com sort.Sort(valueSet(dbSetArray)) sort.Sort(valueSet(extSetArray)) + dupCheck := map[string][]reflect.Value{} + for i := range extSetArray { + _, ok := dupCheck[extSetArray[i].key] + if !ok { + dupCheck[extSetArray[i].key] = []reflect.Value{} + } + dupCheck[extSetArray[i].key] = append(dupCheck[extSetArray[i].key], extSetArray[i].value) + + if len(dupCheck[extSetArray[i].key]) > 1 { + return errors.Wrapf(errors.ErrDuplicateId, "duplicated id: %s", extSetArray[i].key) + } + } + removedValue := reflect.Indirect(reflect.ValueOf(removed)) commonDBValue := reflect.Indirect(reflect.ValueOf(commonDB)) commonExtValue := reflect.Indirect(reflect.ValueOf(commonExt))