service.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  1. // Copyright 2019 Yunion
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package demo
  15. import (
  16. "context"
  17. "fmt"
  18. "net/http"
  19. "net/url"
  20. "os"
  21. "strings"
  22. "yunion.io/x/jsonutils"
  23. "yunion.io/x/log"
  24. "yunion.io/x/pkg/errors"
  25. "yunion.io/x/pkg/util/httputils"
  26. "yunion.io/x/pkg/util/samlutils"
  27. "yunion.io/x/structarg"
  28. "yunion.io/x/onecloud/pkg/appsrv"
  29. "yunion.io/x/onecloud/pkg/httperrors"
  30. "yunion.io/x/onecloud/pkg/i18n"
  31. "yunion.io/x/onecloud/pkg/util/fileutils2"
  32. "yunion.io/x/onecloud/pkg/util/samlutils/idp"
  33. "yunion.io/x/onecloud/pkg/util/samlutils/sp"
  34. )
  35. type Options struct {
  36. Help bool `help:"show help"`
  37. Cert string `help:"certificate file"`
  38. Key string `help:"certificate private key file"`
  39. Port int `help:"listening port"`
  40. Entity string `help:"SAML entityID"`
  41. IdpId string `help:"IDP ID"`
  42. SpMeta []string `help:"ServiceProvider metadata filename"`
  43. IdpMeta []string `help:"IdentityProvider metadata filename"`
  44. }
  45. func showErrorAndExit(e error) {
  46. fmt.Fprintf(os.Stderr, "%s", e)
  47. fmt.Fprintln(os.Stderr)
  48. os.Exit(1)
  49. }
  50. func StartServer() {
  51. err := prepareServer()
  52. if err != nil {
  53. showErrorAndExit(err)
  54. } else {
  55. fmt.Println("exit cleanly")
  56. }
  57. }
  58. func prepareServer() error {
  59. parser, err := structarg.NewArgumentParser(
  60. &Options{},
  61. "samldemo",
  62. "A demo SAML 2.0 https server",
  63. `See "ipmicli help COMMAND" for help on a specific command.`,
  64. )
  65. if err != nil {
  66. return errors.Wrap(err, "NewArgumentParser")
  67. }
  68. err = parser.ParseArgs(os.Args[1:], false)
  69. options := parser.Options().(*Options)
  70. if options.Help {
  71. fmt.Print(parser.HelpString())
  72. return nil
  73. }
  74. if len(options.Entity) == 0 {
  75. return errors.Wrap(httperrors.ErrInputParameter, "empty entityID")
  76. }
  77. if options.Port <= 0 {
  78. return errors.Wrap(httperrors.ErrInputParameter, "port must be positive integer")
  79. }
  80. if len(options.Key) == 0 {
  81. return errors.Wrap(httperrors.ErrInputParameter, "key file must be present")
  82. }
  83. if !fileutils2.Exists(options.Key) {
  84. return errors.Wrapf(httperrors.ErrInputParameter, "key %s not found", options.Key)
  85. }
  86. if len(options.Cert) == 0 {
  87. return errors.Wrap(httperrors.ErrInputParameter, "cert file must be present")
  88. }
  89. if !fileutils2.Exists(options.Cert) {
  90. return errors.Wrapf(httperrors.ErrInputParameter, "cert %s not found", options.Cert)
  91. }
  92. app := appsrv.NewApplication("samldemo", 4, 10, false)
  93. saml, err := samlutils.NewSAMLInstance(options.Entity, options.Cert, options.Key)
  94. if err != nil {
  95. return errors.Wrap(err, "NewSAMLInstance")
  96. }
  97. spFunc := func(ctx context.Context, idpId string, sp *idp.SSAMLServiceProvider) (samlutils.SSAMLSpInitiatedLoginData, error) {
  98. log.Debugf("Recive SP initiated Login: %s", sp.GetEntityId())
  99. data := samlutils.SSAMLSpInitiatedLoginData{}
  100. switch sp.GetEntityId() {
  101. case "https://auth.huaweicloud.com/": // 华为云 SSO
  102. data.NameId = "yunionoss"
  103. data.NameIdFormat = samlutils.NAME_ID_FORMAT_TRANSIENT
  104. data.AudienceRestriction = sp.GetEntityId()
  105. for k, v := range map[string]string{
  106. // "xUserId": "052d45a3e70010440f92c000d9e3f260",
  107. // "xAccountId": "052d45a3e70010440f92c000d9e3f260",
  108. // "bpId": "c58a60a2e0a046c8afa77286924c2b0d",
  109. // "name": "yunionoss",
  110. // "email": "qiujian@yunion.cn",
  111. // "mobile": "13811299225",
  112. "User": "ec2admin",
  113. "Group": "ec2admin",
  114. } {
  115. data.Attributes = append(data.Attributes, samlutils.SSAMLResponseAttribute{
  116. Name: k, FriendlyName: k,
  117. NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
  118. Values: []string{v},
  119. })
  120. }
  121. case "https://samltest.id/saml/sp": // samltest.id SSO
  122. data.NameId = "yunion"
  123. data.NameIdFormat = samlutils.NAME_ID_FORMAT_TRANSIENT
  124. data.AudienceRestriction = sp.GetEntityId()
  125. for _, v := range []struct {
  126. name string
  127. friendlyName string
  128. value string
  129. }{
  130. {
  131. name: "urn:oid:0.9.2342.19200300.100.1.1",
  132. friendlyName: "uid",
  133. value: "9646D89D-F5E7-F0E4-C545A9B2F4B7956B",
  134. },
  135. {
  136. name: "urn:oid:0.9.2342.19200300.100.1.3",
  137. friendlyName: "mail",
  138. value: "samltest@yunion.io",
  139. },
  140. {
  141. name: "urn:oid:2.5.4.4",
  142. friendlyName: "sn",
  143. value: "Jian",
  144. },
  145. {
  146. name: "urn:oid:2.5.4.42",
  147. friendlyName: "givenName",
  148. value: "Jian",
  149. },
  150. } {
  151. data.Attributes = append(data.Attributes, samlutils.SSAMLResponseAttribute{
  152. Name: v.name,
  153. FriendlyName: v.friendlyName,
  154. NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
  155. Values: []string{v.value},
  156. })
  157. }
  158. case "cloud.tencent.com": // 腾讯云 role SSO
  159. data.NameId = "cvmcosreadonly"
  160. data.NameIdFormat = samlutils.NAME_ID_FORMAT_TRANSIENT
  161. data.AudienceRestriction = "https://cloud.tencent.com"
  162. for _, v := range []struct {
  163. name string
  164. friendlyName string
  165. value string
  166. }{
  167. {
  168. name: "https://cloud.tencent.com/SAML/Attributes/Role",
  169. friendlyName: "RoleEntitlement",
  170. value: "qcs::cam::uin/100008182714:roleName/cvmcosreadonly,qcs::cam::uin/100008182714:saml-provider/saml.yunion.io",
  171. },
  172. {
  173. name: "https://cloud.tencent.com/SAML/Attributes/RoleSessionName",
  174. friendlyName: "RoleSessionName",
  175. value: "cvmcosreadonly",
  176. },
  177. } {
  178. data.Attributes = append(data.Attributes, samlutils.SSAMLResponseAttribute{
  179. Name: v.name,
  180. FriendlyName: v.friendlyName,
  181. Values: []string{v.value},
  182. })
  183. }
  184. case "urn:federation:MicrosoftOnline":
  185. data.NameId = sp.Username
  186. data.NameIdFormat = samlutils.NAME_ID_FORMAT_PERSISTENT
  187. data.AudienceRestriction = sp.GetEntityId()
  188. for _, v := range []struct {
  189. name string
  190. friendlyName string
  191. value string
  192. }{
  193. {
  194. name: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
  195. value: data.NameId,
  196. },
  197. } {
  198. data.Attributes = append(data.Attributes, samlutils.SSAMLResponseAttribute{
  199. Name: v.name,
  200. FriendlyName: v.friendlyName,
  201. Values: []string{v.value},
  202. })
  203. }
  204. return data, nil
  205. case "google.com/a/yunion-hk.com":
  206. data.NameId = "qiujian"
  207. data.NameIdFormat = samlutils.NAME_ID_FORMAT_TRANSIENT
  208. data.AudienceRestriction = sp.GetEntityId()
  209. for k, v := range map[string]string{
  210. "user.email": "qiujian@yunion-hk.com",
  211. } {
  212. data.Attributes = append(data.Attributes, samlutils.SSAMLResponseAttribute{
  213. Name: k, FriendlyName: k,
  214. NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
  215. Values: []string{v},
  216. })
  217. }
  218. case "google.com":
  219. data.NameId = "qiujian@yunion-hk.com"
  220. data.NameIdFormat = samlutils.NAME_ID_FORMAT_EMAIL
  221. data.AudienceRestriction = sp.GetEntityId()
  222. for k, v := range map[string]string{
  223. "user.email": "qiujian@yunion-hk.com",
  224. } {
  225. data.Attributes = append(data.Attributes, samlutils.SSAMLResponseAttribute{
  226. Name: k, FriendlyName: k,
  227. NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
  228. Values: []string{v},
  229. })
  230. }
  231. }
  232. return data, nil
  233. }
  234. idpFunc := func(ctx context.Context, sp *idp.SSAMLServiceProvider, idpId, redirectUrl string) (samlutils.SSAMLIdpInitiatedLoginData, error) {
  235. log.Debugf("Recive IDP initiated Login: %s", sp.GetEntityId())
  236. data := samlutils.SSAMLIdpInitiatedLoginData{}
  237. switch sp.GetEntityId() {
  238. case "urn:alibaba:cloudcomputing": // 阿里云role SSO
  239. data.NameId = "ecsossreadonly"
  240. data.NameIdFormat = samlutils.NAME_ID_FORMAT_PERSISTENT
  241. data.AudienceRestriction = sp.GetEntityId()
  242. for k, v := range map[string]string{
  243. "https://www.aliyun.com/SAML-Role/Attributes/Role": "acs:ram::1123247935774897:role/administrator,acs:ram::1123247935774897:saml-provider/saml.yunion.io",
  244. "https://www.aliyun.com/SAML-Role/Attributes/RoleSessionName": "ecsossreadonly",
  245. "https://www.aliyun.com/SAML-Role/Attributes/SessionDuration": "1800",
  246. } {
  247. data.Attributes = append(data.Attributes, samlutils.SSAMLResponseAttribute{
  248. Name: k,
  249. Values: []string{v},
  250. })
  251. }
  252. data.RelayState = "https://homenew.console.aliyun.com/"
  253. case "urn:amazon:webservices:cn-north-1": // AWS CN role SSO
  254. data.NameId = "ec2s3readonly"
  255. data.NameIdFormat = samlutils.NAME_ID_FORMAT_PERSISTENT
  256. data.AudienceRestriction = "https://signin.amazonaws.cn/saml"
  257. for _, v := range []struct {
  258. name string
  259. friendlyName string
  260. value string
  261. }{
  262. {
  263. name: "https://aws.amazon.com/SAML/Attributes/Role",
  264. friendlyName: "RoleEntitlement",
  265. value: "arn:aws-cn:iam::248697896586:role/ec2s3readonly,arn:aws-cn:iam::248697896586:saml-provider/saml.yunion.io",
  266. },
  267. {
  268. name: "https://aws.amazon.com/SAML/Attributes/RoleSessionName",
  269. friendlyName: "RoleSessionName",
  270. value: "ec2s3readonly",
  271. },
  272. {
  273. name: "urn:oid:1.3.6.1.4.1.5923.1.1.1.3",
  274. friendlyName: "eduPersonOrgDN",
  275. value: "ec2s3readonly",
  276. },
  277. } {
  278. data.Attributes = append(data.Attributes, samlutils.SSAMLResponseAttribute{
  279. Name: v.name,
  280. FriendlyName: v.friendlyName,
  281. Values: []string{v.value},
  282. })
  283. }
  284. data.RelayState = "https://console.amazonaws.cn/"
  285. case "urn:amazon:webservices": // AWS Global role SSO
  286. data.NameId = "ec2s3readonly"
  287. data.NameIdFormat = samlutils.NAME_ID_FORMAT_PERSISTENT
  288. data.AudienceRestriction = "https://signin.aws.amazon.com/saml"
  289. for _, v := range []struct {
  290. name string
  291. friendlyName string
  292. value string
  293. }{
  294. {
  295. name: "https://aws.amazon.com/SAML/Attributes/Role",
  296. friendlyName: "RoleEntitlement",
  297. value: "arn:aws:iam::285906155448:role/ec2s3readonly,arn:aws:iam::285906155448:saml-provider/saml.yunion.cn",
  298. },
  299. {
  300. name: "https://aws.amazon.com/SAML/Attributes/RoleSessionName",
  301. friendlyName: "RoleSessionName",
  302. value: "ec2s3readonly",
  303. },
  304. {
  305. name: "urn:oid:1.3.6.1.4.1.5923.1.1.1.3",
  306. friendlyName: "eduPersonOrgDN",
  307. value: "ec2s3readonly",
  308. },
  309. } {
  310. data.Attributes = append(data.Attributes, samlutils.SSAMLResponseAttribute{
  311. Name: v.name,
  312. FriendlyName: v.friendlyName,
  313. Values: []string{v.value},
  314. })
  315. }
  316. data.RelayState = "https://console.aws.amazon.com/"
  317. case "cloud.tencent.com": // 腾讯云 role SSO
  318. data.NameId = "cvmcosreadonly"
  319. data.NameIdFormat = samlutils.NAME_ID_FORMAT_TRANSIENT
  320. data.AudienceRestriction = "https://cloud.tencent.com"
  321. for _, v := range []struct {
  322. name string
  323. friendlyName string
  324. value string
  325. }{
  326. {
  327. name: "https://cloud.tencent.com/SAML/Attributes/Role",
  328. friendlyName: "RoleEntitlement",
  329. value: "qcs::cam::uin/100008182714:roleName/cvmcosreadonly,qcs::cam::uin/100008182714:saml-provider/saml.yunion.io",
  330. },
  331. {
  332. name: "https://cloud.tencent.com/SAML/Attributes/RoleSessionName",
  333. friendlyName: "RoleSessionName",
  334. value: "cvmcosreadonly",
  335. },
  336. } {
  337. data.Attributes = append(data.Attributes, samlutils.SSAMLResponseAttribute{
  338. Name: v.name,
  339. FriendlyName: v.friendlyName,
  340. Values: []string{v.value},
  341. })
  342. }
  343. data.RelayState = "https://console.cloud.tencent.com/"
  344. }
  345. return data, nil
  346. }
  347. logoutFunc := func(ctx context.Context, idpId string) string {
  348. return fmt.Sprintf(`<!DOCTYPE html><html lang="zh_CN"><head><meta charset="utf-8"><meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head><body><h1>成功退出登录,<a href="%s">重新登录</a></h1></body></html>`, httputils.JoinPath(options.Entity, "SAML/idp"))
  349. }
  350. idpInst := idp.NewIdpInstance(saml, spFunc, idpFunc, logoutFunc)
  351. for _, spMetaFile := range options.SpMeta {
  352. err := idpInst.AddSPMetadataFile(spMetaFile)
  353. if err != nil {
  354. return errors.Wrapf(err, "AddSPMetadataFile %s", spMetaFile)
  355. }
  356. }
  357. idpInst.AddHandlers(app, "SAML/idp", nil)
  358. idpInst.SetHtmlTemplate(i18n.NewTableEntry().CN(`<!DOCTYPE html><html lang="zh_CN"><head><meta charset="utf-8"><meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head><body><h1>正在跳转到云控制台,请等待。。。</h1>$FORM$</body></html>`))
  359. app.AddHandler("GET", "SAML/idp", func(ctx context.Context, w http.ResponseWriter, r *http.Request) {
  360. idpInitUrl := httputils.JoinPath(options.Entity, "SAML/idp/sso")
  361. htmlBuf := strings.Builder{}
  362. htmlBuf.WriteString(`<!doctype html><html lang=en><body><ol>`)
  363. // IDP initiated
  364. for _, v := range []struct {
  365. name string
  366. entityID string
  367. }{
  368. {
  369. name: "Aliyun Role SSO",
  370. entityID: "urn:alibaba:cloudcomputing",
  371. },
  372. {
  373. name: "AWS CN Role SSO",
  374. entityID: "urn:amazon:webservices:cn-north-1",
  375. },
  376. {
  377. name: "AWS Global Role SSO",
  378. entityID: "urn:amazon:webservices",
  379. },
  380. {
  381. name: "Tencent Cloud Role SSO",
  382. entityID: "cloud.tencent.com",
  383. },
  384. } {
  385. query := samlutils.SIdpInitiatedLoginInput{
  386. EntityID: v.entityID,
  387. IdpId: options.IdpId,
  388. }
  389. htmlBuf.WriteString(fmt.Sprintf(`<li><a href="%s?%s">%s (IDP-Initiated)</a></li>`, idpInitUrl, jsonutils.Marshal(query).QueryString(), v.name))
  390. }
  391. for _, v := range []struct {
  392. name string
  393. url string
  394. }{
  395. /*{
  396. name: "Huawei cloud partner SSO",
  397. url: "https://auth.huaweicloud.com/authui/saml/login?xAccountType=yunion_IDP&isFirstLogin=false&service=https%3a%2f%2fconsole.huaweicloud.com%2fiam%2f",
  398. },*/
  399. {
  400. name: "Huawei cloud SSO",
  401. url: "https://auth.huaweicloud.com/authui/federation/websso?domain_id=052d45a3e70010440f92c000d9e3f260&idp=yunion&protocol=saml",
  402. },
  403. {
  404. name: "Tencent cloud SSO",
  405. url: "https://cloud.tencent.com/login/forwardIdp/100008182714/saml.yunion.io",
  406. },
  407. {
  408. name: "Google cloud SSO",
  409. url: "https://www.google.com/a/yunion-hk.com/ServiceLogin?continue=https://console.cloud.google.com",
  410. },
  411. {
  412. name: "Azure cloud SSO",
  413. 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",
  414. },
  415. } {
  416. htmlBuf.WriteString(fmt.Sprintf(`<li><a href="%s">%s (SP-Initiated)</a></li>`, v.url, v.name))
  417. }
  418. htmlBuf.WriteString(`</ol></body></html>`)
  419. appsrv.SendHTML(w, htmlBuf.String())
  420. })
  421. consumeFunc := func(ctx context.Context, w http.ResponseWriter, idp *sp.SSAMLIdentityProvider, result sp.SSAMLAssertionConsumeResult) error {
  422. html := strings.Builder{}
  423. html.WriteString("<!doctype html><html lang=en><head><meta charset=\"utf-8\"><meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"></head><body><ol>")
  424. html.WriteString(fmt.Sprintf("<li>RequestId: %s</li>", result.RequestID))
  425. html.WriteString(fmt.Sprintf("<li>RelayState: %s</li>", result.RelayState))
  426. for _, v := range result.Attributes {
  427. html.WriteString(fmt.Sprintf("<li>%s(%s): %s</li>", v.Name, v.FriendlyName, v.Values))
  428. }
  429. html.WriteString("</ol></body></html>")
  430. appsrv.SendHTML(w, html.String())
  431. return nil
  432. }
  433. spLoginFunc := func(ctx context.Context, idp *sp.SSAMLIdentityProvider) (sp.SSAMLSpInitiatedLoginRequest, error) {
  434. result := sp.SSAMLSpInitiatedLoginRequest{}
  435. result.RequestID = samlutils.GenerateSAMLId()
  436. return result, nil
  437. }
  438. spInst := sp.NewSpInstance(saml, "Yunion SAML Demo Service", consumeFunc, spLoginFunc)
  439. for _, idpFile := range options.IdpMeta {
  440. err := spInst.AddIdpMetadataFile(idpFile)
  441. if err != nil {
  442. return errors.Wrap(err, "AddIdpMetadataFile")
  443. }
  444. }
  445. spInst.AddHandlers(app, "SAML/sp")
  446. app.AddHandler("GET", "SAML/sp", func(ctx context.Context, w http.ResponseWriter, r *http.Request) {
  447. spInitUrl := httputils.JoinPath(options.Entity, "SAML/sp/sso")
  448. htmlBuf := strings.Builder{}
  449. htmlBuf.WriteString(`<!doctype html><html lang=en><body><ol>`)
  450. for _, idp := range spInst.GetIdentityProviders() {
  451. entityId := idp.GetEntityId()
  452. htmlBuf.WriteString(fmt.Sprintf(`<li><a href="%s?EntityID=%s">%s (SP-Initiated)</a></li>`, spInitUrl, url.QueryEscape(entityId), entityId))
  453. }
  454. htmlBuf.WriteString(`</ol></body></html>`)
  455. appsrv.SendHTML(w, htmlBuf.String())
  456. })
  457. addr := fmt.Sprintf(":%d", options.Port)
  458. app.ListenAndServeTLS(addr, options.Cert, options.Key)
  459. return nil
  460. }