Skip to content

Commit 9a474b2

Browse files
committed
feat: implement SAML2 authentication with configuration options
1 parent a89b1ff commit 9a474b2

File tree

9 files changed

+334
-6
lines changed

9 files changed

+334
-6
lines changed

apps/oss/views/file.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# coding=utf-8
22
import base64
3+
import ipaddress
4+
import socket
5+
from urllib.parse import urlparse
36

47
import requests
58
from django.utils.translation import gettext_lazy as _
@@ -83,7 +86,17 @@ class GetUrlView(APIView):
8386
)
8487
def get(self, request: Request):
8588
url = request.query_params.get('url')
86-
response = requests.get(url)
89+
parsed = validate_url(url)
90+
91+
response = requests.get(
92+
url,
93+
timeout=3,
94+
allow_redirects=False
95+
)
96+
final_host = urlparse(response.url).hostname
97+
if is_private_ip(final_host):
98+
raise ValueError("Blocked unsafe redirect to internal host")
99+
87100
# 返回状态码 响应内容大小 响应的contenttype 还有字节流
88101
content_type = response.headers.get('Content-Type', '')
89102
# 根据内容类型决定如何处理
@@ -99,3 +112,43 @@ def get(self, request: Request):
99112
'Content-Type': content_type,
100113
'content': content,
101114
})
115+
116+
117+
def is_private_ip(host: str) -> bool:
118+
"""检测 IP 是否属于内网、环回、云 metadata 的危险地址"""
119+
try:
120+
ip = ipaddress.ip_address(socket.gethostbyname(host))
121+
return (
122+
ip.is_private or
123+
ip.is_loopback or
124+
ip.is_reserved or
125+
ip.is_link_local or
126+
ip.is_multicast
127+
)
128+
except Exception:
129+
return True
130+
131+
132+
def validate_url(url: str):
133+
"""验证 URL 是否安全"""
134+
if not url:
135+
raise ValueError("URL is required")
136+
137+
parsed = urlparse(url)
138+
139+
# 仅允许 http / https
140+
if parsed.scheme not in ("http", "https"):
141+
raise ValueError("Only http and https are allowed")
142+
143+
host = parsed.hostname
144+
path = parsed.path
145+
146+
# 域名不能为空
147+
if not host:
148+
raise ValueError("Invalid URL")
149+
150+
# 禁止访问内部、保留、环回、云 metadata
151+
if is_private_ip(host):
152+
raise ValueError("Access to internal IP addresses is blocked")
153+
154+
return parsed

ui/src/api/user/login.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,11 @@ const postLanguage: (data: any, loading?: Ref<boolean>) => Promise<Result<User>>
9999
) => {
100100
return post('/user/language', data, undefined, loading)
101101
}
102-
102+
const samlLogin: (loading?: Ref<boolean>) => Promise<Result<any>> = (
103+
loading,
104+
) => {
105+
return get('/saml2', '', loading)
106+
}
103107
export default {
104108
login,
105109
logout,
@@ -112,5 +116,6 @@ export default {
112116
getDingOauth2Callback,
113117
getLarkCallback,
114118
getQrSource,
115-
ldapLogin
119+
ldapLogin,
120+
samlLogin
116121
}

ui/src/locales/lang/en-US/views/system.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,24 @@ export default {
7171
filedMappingPlaceholder: 'Please enter field mapping',
7272
enableAuthentication: 'Enable OAuth2 Authentication',
7373
},
74+
saml2: {
75+
title: 'SAML2',
76+
ldp: 'Idp MetaData Url',
77+
ldpPlaceholder: 'Please enter Idp MetaData Url',
78+
enableAuthnRequests: 'Enable request signature',
79+
enableAssertions: 'Enable assertion signatures',
80+
privateKey: 'SP Private Key',
81+
privateKeyPlaceholder: 'Please enter SP Private Key',
82+
certificate: 'SP Certificate',
83+
certificatePlaceholder: 'Please enter SP Certificate',
84+
filedMapping: 'Field Mapping',
85+
spEntityId: 'SP Entity Id',
86+
spEntityIdPlaceholder: 'Please enter SP Entity Id',
87+
spAcs: 'SP Ace',
88+
spAcsPlaceholder: 'Please enter SP Ace',
89+
filedMappingPlaceholder: 'Please enter field mapping',
90+
enableAuthentication: 'Enable SAML2 Authentication',
91+
},
7492
scanTheQRCode: {
7593
title: 'Scan the QR code',
7694
wecom: 'WeCom',
@@ -133,9 +151,9 @@ export default {
133151
type: 'Type',
134152
management: 'management',
135153
},
136-
default_login: 'Default Login Method',
154+
default_login: 'Default Login Method',
137155
display_code: 'Account login verification code setting',
138-
loginFailed: 'Login failed',
156+
loginFailed: 'Login failed',
139157
loginFailedMessage: 'Display verification code twice',
140158
display_codeTip: 'When the value is -1, the verification code is not displayed',
141159
time: 'Times',

ui/src/locales/lang/zh-CN/views/system.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,24 @@ export default {
7373
filedMappingPlaceholder: '请输入字段映射',
7474
enableAuthentication: '启用 OAuth2 认证',
7575
},
76+
saml2: {
77+
title: 'SAML2',
78+
ldp: 'Idp MetaData Url',
79+
ldpPlaceholder: '请输入 Idp MetaData Url',
80+
enableAuthnRequests: '开启请求签名',
81+
enableAssertions: '开启断言签名',
82+
privateKey: 'SP Private Key',
83+
privateKeyPlaceholder: '请输入 SP Private Key',
84+
certificate: 'SP Certificate',
85+
certificatePlaceholder: '请输入 SP Certificate',
86+
filedMapping: '字段映射',
87+
spEntityId: 'SP Entity Id',
88+
spEntityIdPlaceholder: '请输入 SP Entity Id',
89+
spAcs: 'SP Ace',
90+
spAcsPlaceholder: '请输入 SP Ace',
91+
filedMappingPlaceholder: '请输入字段映射',
92+
enableAuthentication: '启用 SAML2 认证',
93+
},
7694
scanTheQRCode: {
7795
title: '扫码登录',
7896
wecom: '企业微信',

ui/src/locales/lang/zh-Hant/views/system.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,25 @@ export default {
7171
filedMappingPlaceholder: '請輸入欄位對應',
7272
enableAuthentication: '啟用 OAuth2 認證',
7373
},
74+
saml2: {
75+
title: 'SAML2',
76+
ldp: 'Idp MetaData Url',
77+
ldpPlaceholder: '請輸入 Idp MetaData Url',
78+
enableAuthnRequests: '開啟請求簽名',
79+
enableAssertions: '開啟斷言簽名',
80+
privateKey: 'SP Private Key',
81+
privateKeyPlaceholder: '請輸入 SP Private Key',
82+
certificate: 'SP Certificate',
83+
certificatePlaceholder: '請輸入 SP Certificate',
84+
filedMapping: '欄位映射',
85+
spEntityId: 'SP Entity Id',
86+
spEntityIdPlaceholder: '請輸入 SP Entity Id',
87+
spAcs: 'SP Ace',
88+
spAcsPlaceholder: '請輸入 SP Ace',
89+
filedMappingPlaceholder: '請輸入欄位映射',
90+
enableAuthentication: '啟用 SAML2 認證',
91+
},
92+
7493
scanTheQRCode: {
7594
title: '掃碼登入',
7695
wecom: '企業微信',
@@ -133,7 +152,7 @@ export default {
133152
type: '類型',
134153
management: '管理',
135154
},
136-
default_login: '預設登入方式',
155+
default_login: '預設登入方式',
137156
display_code: '帳號登入驗證碼設定',
138157
loginFailed: '登入失敗',
139158
loginFailedMessage: '次顯示驗證碼',

ui/src/stores/modules/login.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ const useLoginStore = defineStore('login', {
8686
return ok.data
8787
})
8888
},
89+
async samlLogin() {
90+
return LoginApi.samlLogin().then((ok) => {
91+
})
92+
}
8993
},
9094
})
9195

ui/src/views/login/index.vue

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,10 @@ function redirectAuth(authType: string, needMessage: boolean = true) {
329329
if (config.scope) {
330330
url += `&scope=${config.scope}`
331331
}
332+
} else if (authType === 'SAML2') {
333+
loginApi.samlLogin().then((res: any) => {
334+
window.location.href = res.data
335+
})
332336
}
333337
if (!url) {
334338
return

0 commit comments

Comments
 (0)