feat(cloudid): add azure saml

This commit is contained in:
Qu Xuan
2021-01-18 14:58:19 +08:00
parent be959d9459
commit 627503cdc1
37 changed files with 916 additions and 32 deletions
@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:alg="urn:oasis:names:tc:SAML:metadata:algsupport" ID="_1e55eaf7-5cb9-4601-b557-e322fdfb2baf" entityID="urn:federation:MicrosoftOnline">
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignedInfo>
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
<Reference URI="#_1e55eaf7-5cb9-4601-b557-e322fdfb2baf">
<Transforms>
<Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
<Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
</Transforms>
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
<DigestValue>fA33hUaj24/8F8bArDIkH6CpX0k=</DigestValue>
</Reference>
</SignedInfo>
<SignatureValue>p/rlVWxLRIYiHozzWUgsjmQ5ItMZ/ITeEOg/edeo50l7qU8wLM9JD55EHoLpVvxImBcCfY1CxKdq3s1o9Up1jvEcH9KjfYZea1q8hzTkQdt7F2XvxoqDFRLvVwyHHJjygaEvFRXXxml7IDgDx9ScSRhQeGUlTGhI9eAFSaGkChM/FPnmekZ3ngSwbVf4FsTLIArxMqEnV0lXNMTr5NHQ6VL5wZ/cHLNCkkBoY0qWyv2hnYiZiXa4TKjpAN+8QF5GDPv/0ExJwe+aoRAQhdBzhBUICQdQd+v3Rb5SuX++iVfmf4IPMqj8CK4uK7iYXNLCGEO8oFR2KlK/zBpHzw6yCQ==</SignatureValue>
<KeyInfo>
<X509Data>
<X509Certificate>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==</X509Certificate>
</X509Data>
</KeyInfo>
</Signature>
<SPSSODescriptor WantAssertionsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<KeyDescriptor use="signing">
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509Certificate>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==</X509Certificate>
</X509Data>
</KeyInfo>
</KeyDescriptor>
<KeyDescriptor use="signing">
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509Certificate>MIIC/TCCAeWgAwIBAgIQbgDHfi3t1JNGVqwD5/7lmjANBgkqhkiG9w0BAQsFADApMScwJQYDVQQDEx5MaXZlIElEIFNUUyBTaWduaW5nIFB1YmxpYyBLZXkwHhcNMjAxMjIxMDAwMDAwWhcNMjUxMjIxMDAwMDAwWjApMScwJQYDVQQDEx5MaXZlIElEIFNUUyBTaWduaW5nIFB1YmxpYyBLZXkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDFT0/0/2qQurnYa0LbJHF9YYozhEH6r9mCxVDBYbewSG4tGgrWpsewQ/96pcczGMQctMvU+h2eX38Hx/f9JAIDbuRQzQlsPhQS7DDZ6WlTXU+t8d/g2C7fpSoLs4KVdJih4xyjLUWj+BK/ijsRjBt4Riw9VbJH/DdWKyoSMbECEiE+s1RtLP/eYoMmNfxyQGqWirCNqVNBTlqzYQp4dgF0foYy4ktoxwmQOVoTcIMFYp1I4pFPI7CxuMLkfK0X7aTbM7YGphvMfJxJkjrQdyI7G5d1t4DNi3zkEbBT7FGAr6qPt3Kn9ralpqJKHdpEBA9N0vNwQo5XTYIhUbPQ16IRAgMBAAGjITAfMB0GA1UdDgQWBBRs7tPmfkksSr67KtElHjYZbeaCTjANBgkqhkiG9w0BAQsFAAOCAQEAJqwMZSjQJ36x+1sty6EeLKQLQewQwPaEC47Zut+8bXed6Q8jMZ0bfa/MM7XquEcabaMZLQuKLft44YXwXXQOfQrI2qjQr3eToJFlDT9hR0rfp9wQqttDxd6Aa6RWwDTgo5oKUQCTKLHhEy8uWzScK0eGt2d7TWTaDXjRSwNq6tM7fRhZs07tKBV3xfi9EQy/mlavAMFRBVm86NSo7AsOG1IOMq03U3ooCWAXh9PdvvHNfHhH19futAnC/HeOjwRF1Qc527aBMphYFQLdiThfmfmiE/AhQqCwZ2oE7uCJhBtR+Kb1ZGhjI35pHfsSqGiFa7Kr+5ave822PDcke89Mvg==</X509Certificate>
</X509Data>
</KeyInfo>
</KeyDescriptor>
<KeyDescriptor use="signing">
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509Certificate>MIIC/TCCAeWgAwIBAgIQbgDHfi3t1JNGVqwD5/7lmjANBgkqhkiG9w0BAQsFADApMScwJQYDVQQDEx5MaXZlIElEIFNUUyBTaWduaW5nIFB1YmxpYyBLZXkwHhcNMjAxMjIxMDAwMDAwWhcNMjUxMjIxMDAwMDAwWjApMScwJQYDVQQDEx5MaXZlIElEIFNUUyBTaWduaW5nIFB1YmxpYyBLZXkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDFT0/0/2qQurnYa0LbJHF9YYozhEH6r9mCxVDBYbewSG4tGgrWpsewQ/96pcczGMQctMvU+h2eX38Hx/f9JAIDbuRQzQlsPhQS7DDZ6WlTXU+t8d/g2C7fpSoLs4KVdJih4xyjLUWj+BK/ijsRjBt4Riw9VbJH/DdWKyoSMbECEiE+s1RtLP/eYoMmNfxyQGqWirCNqVNBTlqzYQp4dgF0foYy4ktoxwmQOVoTcIMFYp1I4pFPI7CxuMLkfK0X7aTbM7YGphvMfJxJkjrQdyI7G5d1t4DNi3zkEbBT7FGAr6qPt3Kn9ralpqJKHdpEBA9N0vNwQo5XTYIhUbPQ16IRAgMBAAGjITAfMB0GA1UdDgQWBBRs7tPmfkksSr67KtElHjYZbeaCTjANBgkqhkiG9w0BAQsFAAOCAQEAJqwMZSjQJ36x+1sty6EeLKQLQewQwPaEC47Zut+8bXed6Q8jMZ0bfa/MM7XquEcabaMZLQuKLft44YXwXXQOfQrI2qjQr3eToJFlDT9hR0rfp9wQqttDxd6Aa6RWwDTgo5oKUQCTKLHhEy8uWzScK0eGt2d7TWTaDXjRSwNq6tM7fRhZs07tKBV3xfi9EQy/mlavAMFRBVm86NSo7AsOG1IOMq03U3ooCWAXh9PdvvHNfHhH19futAnC/HeOjwRF1Qc527aBMphYFQLdiThfmfmiE/AhQqCwZ2oE7uCJhBtR+Kb1ZGhjI35pHfsSqGiFa7Kr+5ave822PDcke89Mvg==</X509Certificate>
</X509Data>
</KeyInfo>
</KeyDescriptor>
<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://login.microsoftonline.com/login.srf"/>
<NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</NameIDFormat>
<NameIDFormat>urn:mace:shibboleth:1.0:nameIdentifier</NameIDFormat>
<NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</NameIDFormat>
<NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
<NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</NameIDFormat>
<AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://login.microsoftonline.com/login.srf" index="0" isDefault="true"/>
<AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign" Location="https://login.microsoftonline.com/login.srf" index="1"/>
<AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:PAOS" Location="https://login.microsoftonline.com/login.srf" index="2"/>
</SPSSODescriptor>
<Extensions>
<alg:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
<alg:SigningMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
</Extensions>
</EntityDescriptor>
+2 -2
View File
@@ -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
+4 -4
View File
@@ -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=
+58 -5
View File
@@ -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
+43 -1
View File
@@ -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())
}
+1 -1
View File
@@ -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")
+43
View File
@@ -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
}
+1
View File
@@ -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"
+15
View File
@@ -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"
+131
View File
@@ -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(`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="0; url=%s">
<title>waiting...</title>
</head>
<body>
waiting...
</body>
</html>`, 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)
}
@@ -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{})
}
+1
View File
@@ -24,6 +24,7 @@ type SClouduserCreateConfig struct {
Email string
MobilePhone string
ExternalPolicyIds []string
UserType string
}
type SCloudpolicyPermission struct {
+3
View File
@@ -1072,6 +1072,9 @@ type IClouduser interface {
GetGlobalId() string
GetName() string
GetEmailAddr() string
GetInviteUrl() string
GetICloudgroups() ([]ICloudgroup, error)
GetISystemCloudpolicies() ([]ICloudpolicy, error)
+1
View File
@@ -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 {
+1 -1
View File
@@ -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"`
}
+8
View File
@@ -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 {
+8
View File
@@ -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
+8
View File
@@ -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))
}
+52 -4
View File
@@ -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
}
+1 -2
View File
@@ -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)
}
+13
View File
@@ -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)
}
+13
View File
@@ -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
}
+136
View File
@@ -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
}
+12
View File
@@ -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
})
}
@@ -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
})
}
+8
View File
@@ -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 {
+8
View File
@@ -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
}
+8
View File
@@ -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
+25
View File
@@ -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(`<li><a href="%s">%s (SP-Initiated)</a></li>`, v.url, v.name))
}
+8 -4
View File
@@ -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(),
+2
View File
@@ -23,6 +23,8 @@ import (
type SSAMLServiceProvider struct {
desc samlutils.EntityDescriptor
Username string
}
func (sp *SSAMLServiceProvider) GetEntityId() string {
+4 -1
View File
@@ -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{},
},
+2
View File
@@ -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 {
+10 -5
View File
@@ -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) {
+120
View File
@@ -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
}
+3 -2
View File
@@ -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
+15
View File
@@ -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))