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))