diff --git a/object/application.go b/object/application.go index 8a349722..ea638876 100644 --- a/object/application.go +++ b/object/application.go @@ -34,6 +34,12 @@ type SignupItem struct { Rule string `json:"rule"` } +type SamlItem struct { + Name string `json:"name"` + NameFormat string `json:"nameformat"` + Value string `json:"value"` +} + type Application struct { Owner string `xorm:"varchar(100) notnull pk" json:"owner"` Name string `xorm:"varchar(100) notnull pk" json:"name"` @@ -62,6 +68,7 @@ type Application struct { CertPublicKey string `xorm:"-" json:"certPublicKey"` Tags []string `xorm:"mediumtext" json:"tags"` InvitationCodes []string `xorm:"varchar(200)" json:"invitationCodes"` + SamlAttributes []*SamlItem `xorm:"varchar(1000)" json:"samlAttributes"` ClientId string `xorm:"varchar(100)" json:"clientId"` ClientSecret string `xorm:"varchar(100)" json:"clientSecret"` diff --git a/object/saml_idp.go b/object/saml_idp.go index f10f0576..e2909607 100644 --- a/object/saml_idp.go +++ b/object/saml_idp.go @@ -37,7 +37,7 @@ import ( // NewSamlResponse // returns a saml2 response -func NewSamlResponse(user *User, host string, certificate string, destination string, iss string, requestId string, redirectUri []string) (*etree.Element, error) { +func NewSamlResponse(application *Application, user *User, host string, certificate string, destination string, iss string, requestId string, redirectUri []string) (*etree.Element, error) { samlResponse := &etree.Element{ Space: "samlp", Tag: "Response", @@ -103,6 +103,13 @@ func NewSamlResponse(user *User, host string, certificate string, destination st displayName.CreateAttr("NameFormat", "urn:oasis:names:tc:SAML:2.0:attrname-format:basic") displayName.CreateElement("saml:AttributeValue").CreateAttr("xsi:type", "xs:string").Element().SetText(user.DisplayName) + for _, item := range application.SamlAttributes { + role := attributes.CreateElement("saml:Attribute") + role.CreateAttr("Name", item.Name) + role.CreateAttr("NameFormat", item.NameFormat) + role.CreateElement("saml:AttributeValue").CreateAttr("xsi:type", "xs:string").Element().SetText(item.Value) + } + roles := attributes.CreateElement("saml:Attribute") roles.CreateAttr("Name", "Roles") roles.CreateAttr("NameFormat", "urn:oasis:names:tc:SAML:2.0:attrname-format:basic") @@ -184,10 +191,11 @@ type SingleSignOnService struct { type Attribute struct { XMLName xml.Name - Name string `xml:"Name,attr"` - NameFormat string `xml:"NameFormat,attr"` - FriendlyName string `xml:"FriendlyName,attr"` - Xmlns string `xml:"xmlns,attr"` + Name string `xml:"Name,attr"` + NameFormat string `xml:"NameFormat,attr"` + FriendlyName string `xml:"FriendlyName,attr"` + Xmlns string `xml:"xmlns,attr"` + Values []string `xml:"AttributeValue"` } func GetSamlMeta(application *Application, host string) (*IdpEntityDescriptor, error) { @@ -309,7 +317,7 @@ func GetSamlResponse(application *Application, user *User, samlRequest string, h _, originBackend := getOriginFromHost(host) // build signedResponse - samlResponse, _ := NewSamlResponse(user, originBackend, certificate, authnRequest.AssertionConsumerServiceURL, authnRequest.Issuer.Url, authnRequest.ID, application.RedirectUris) + samlResponse, _ := NewSamlResponse(application, user, originBackend, certificate, authnRequest.AssertionConsumerServiceURL, authnRequest.Issuer.Url, authnRequest.ID, application.RedirectUris) randomKeyStore := &X509Key{ PrivateKey: cert.PrivateKey, X509Certificate: certificate, diff --git a/web/src/ApplicationEditPage.js b/web/src/ApplicationEditPage.js index 354cdb88..d2c13821 100644 --- a/web/src/ApplicationEditPage.js +++ b/web/src/ApplicationEditPage.js @@ -28,6 +28,7 @@ import i18next from "i18next"; import UrlTable from "./table/UrlTable"; import ProviderTable from "./table/ProviderTable"; import SignupTable from "./table/SignupTable"; +import SamlAttributeTable from "./table/SamlAttributeTable"; import PromptPage from "./auth/PromptPage"; import copy from "copy-to-clipboard"; import ThemeEditor from "./common/theme/ThemeEditor"; @@ -104,6 +105,7 @@ class ApplicationEditPage extends React.Component { providers: [], uploading: false, mode: props.location.mode !== undefined ? props.location.mode : "edit", + samlAttributes: [], samlMetadata: null, isAuthorized: true, }; @@ -638,6 +640,19 @@ class ApplicationEditPage extends React.Component { }} /> + + + {Setting.getLabel(i18next.t("general:SAML Attribute"), i18next.t("general:SAML Attribute - Tooltip"))} : + + + {this.updateApplicationField("samlAttributes", value);}} + /> + + {Setting.getLabel(i18next.t("application:SAML metadata"), i18next.t("application:SAML metadata - Tooltip"))} : diff --git a/web/src/table/SamlAttributeTable.js b/web/src/table/SamlAttributeTable.js new file mode 100644 index 00000000..32161c7d --- /dev/null +++ b/web/src/table/SamlAttributeTable.js @@ -0,0 +1,162 @@ +// Copyright 2023 The Casdoor Authors. All Rights Reserved. +// +// 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. + +import React from "react"; +import {DeleteOutlined, DownOutlined, UpOutlined} from "@ant-design/icons"; +import {Button, Col, Input, Row, Select, Table, Tooltip} from "antd"; +import * as Setting from "../Setting"; +import i18next from "i18next"; + +const {Option} = Select; + +class SamlAttributeTable extends React.Component { + constructor(props) { + super(props); + this.state = { + classes: props, + }; + } + + updateTable(table) { + this.props.onUpdateTable(table); + } + + updateField(table, index, key, value) { + table[index][key] = value; + this.updateTable(table); + } + + addRow(table) { + const row = {Name: "", nameformat: "", value: ""}; + if (table === undefined || table === null) { + table = []; + } + table = Setting.addRow(table, row); + this.updateTable(table); + } + + deleteRow(table, i) { + table = Setting.deleteRow(table, i); + this.updateTable(table); + } + + upRow(table, i) { + table = Setting.swapRow(table, i - 1, i); + this.updateTable(table); + } + + downRow(table, i) { + table = Setting.swapRow(table, i, i + 1); + this.updateTable(table); + } + + renderTable(table) { + const columns = [ + { + title: i18next.t("user:Name"), + dataIndex: "name", + key: "name", + width: "200px", + render: (text, record, index) => { + return ( + { + this.updateField(table, index, "name", e.target.value); + }} /> + ); + }, + }, + { + title: i18next.t("user:Name format"), + dataIndex: "nameformat", + key: "nameformat", + width: "200px", + render: (text, record, index) => { + return ( + + ); + }, + }, + { + title: i18next.t("user:Value"), + dataIndex: "value", + key: "value", + width: "200px", + render: (text, record, index) => { + return ( + { + this.updateField(table, index, "value", e.target.value); + }} /> + ); + }, + }, + { + title: i18next.t("general:Action"), + dataIndex: "action", + key: "action", + width: "20px", + render: (text, record, index) => { + return ( +
+ +
+ ); + }, + }, + ]; + + return ( + ( +
+ +
+ )} + columns={columns} dataSource={table} rowKey="key" size="middle" bordered + /> + ); + } + + render() { + return ( +
+ +
+ { + this.renderTable(this.props.table) + } + + + + ); + } +} + +export default SamlAttributeTable;