mirror of
https://github.com/AlistGo/alist.git
synced 2025-12-19 02:50:06 +08:00
Some checks failed
beta release / Beta Release Changelog (1.21, ubuntu-latest) (push) Waiting to run
beta release / Beta Release (md5, !(*musl*|*windows-arm64*|*android*|*freebsd*)) (push) Blocked by required conditions
beta release / Beta Release (md5-android, android-*) (push) Blocked by required conditions
beta release / Beta Release (md5-freebsd, freebsd-*) (push) Blocked by required conditions
beta release / Beta Release (md5-linux-musl, linux-!(arm*)-musl*) (push) Blocked by required conditions
beta release / Beta Release (md5-linux-musl-arm, linux-arm*-musl*) (push) Blocked by required conditions
beta release / Beta Release (md5-windows-arm64, windows-arm64) (push) Blocked by required conditions
beta release / Beta Release Desktop (push) Blocked by required conditions
build / Build (ubuntu-latest, android-arm64) (push) Waiting to run
build / Build (ubuntu-latest, darwin-amd64) (push) Waiting to run
build / Build (ubuntu-latest, darwin-arm64) (push) Waiting to run
build / Build (ubuntu-latest, linux-amd64-musl) (push) Waiting to run
build / Build (ubuntu-latest, linux-arm64-musl) (push) Waiting to run
build / Build (ubuntu-latest, windows-amd64) (push) Waiting to run
build / Build (ubuntu-latest, windows-arm64) (push) Waiting to run
release_docker / Build Binaries for Docker Release (push) Waiting to run
release_docker / Release Docker image (, latest, ) (push) Blocked by required conditions
release_docker / Release Docker image (INSTALL_ARIA2=true, aria2, suffix=-aria2,onlatest=true) (push) Blocked by required conditions
release_docker / Release Docker image (INSTALL_FFMPEG=true
INSTALL_ARIA2=true
, aio, suffix=-aio,onlatest=true) (push) Blocked by required conditions
release_docker / Release Docker image (INSTALL_FFMPEG=true, ffmpeg, suffix=-ffmpeg,onlatest=true) (push) Blocked by required conditions
auto_lang / auto generate lang.json (1.21, ubuntu-latest) (push) Has been cancelled
* 标签管理 * pr检查优化 * feat(role): Implement role management functionality - Add role management routes in `server/router.go` for listing, getting, creating, updating, and deleting roles - Introduce `initRoles()` in `internal/bootstrap/data/data.go` for initializing roles during bootstrap - Create `internal/op/role.go` to handle role operations including caching and singleflight - Implement role handler functions in `server/handles/role.go` for API responses - Define database operations for roles in `internal/db/role.go` - Extend `internal/db/db.go` for role model auto-migration - Design `internal/model/role.go` to represent role structure with ID, name, description, base path, and permissions - Initialize default roles (`admin` and `guest`) in `internal/bootstrap/data/role.go` during startup * refactor(user roles): Support multiple roles for users - Change the `Role` field type from `int` to `[]int` in `drivers/alist_v3/types.go` and `drivers/quqi/types.go`. - Update the `Role` field in `internal/model/user.go` to use a new `Roles` type with JSON and database support. - Modify `IsGuest` and `IsAdmin` methods to check for roles using `Contains` method. - Update `GetUserByRole` method in `internal/db/user.go` to handle multiple roles. - Add `roles.go` to define a new `Roles` type with JSON marshalling and scanning capabilities. - Adjust code in `server/handles/user.go` to compare roles with `utils.SliceEqual`. - Change role initialization for users in `internal/bootstrap/data/dev.go` and `internal/bootstrap/data/user.go`. - Update `Role` handling in `server/handles/task.go`, `server/handles/ssologin.go`, and `server/handles/ldap_login.go`. * feat(user/role): Add path limit check for user and role permissions - Add new permission bit for checking path limits in `user.go` - Implement `CheckPathLimit` method in `User` struct to validate path access - Modify `JoinPath` method in `User` to enforce path limit checks - Update `role.go` to include path limit logic in `Role` struct - Document new permission bit in `Role` and `User` comments for clarity * feat(permission): Add role-based permission handling - Introduce `role_perm.go` for managing user permissions based on roles. - Implement `HasPermission` and `MergeRolePermissions` functions. - Update `webdav.go` to utilize role-based permissions instead of direct user checks. - Modify `fsup.go` to integrate `CanAccessWithRoles` function. - Refactor `fsread.go` to use `common.HasPermission` for permission validation. - Adjust `fsmanage.go` for role-based access control checks. - Enhance `ftp.go` and `sftp.go` to manage FTP access via roles. - Update `fsbatch.go` to employ `MergeRolePermissions` for batch operations. - Replace direct user permission checks with role-based permission handling across various modules. * refactor(user): Replace integer role values with role IDs - Change `GetAdmin()` and `GetGuest()` functions to retrieve role by name and use role ID. - Add patch for version `v3.45.2` to convert legacy integer roles to role IDs. - Update `dev.go` and `user.go` to use role IDs instead of integer values for roles. - Remove redundant code in `role.go` related to guest role creation. - Modify `ssologin.go` and `ldap_login.go` to set user roles to nil instead of using integer roles. - Introduce `convert_roles.go` to handle conversion of legacy roles and ensure role existence in the database. * feat(role_perm): implement support for multiple base paths for roles - Modify role permission checks to support multiple base paths - Update role creation and update functions to handle multiple base paths - Add migration script to convert old base_path to base_paths - Define new Paths type for handling multiple paths in the model - Adjust role model to replace BasePath with BasePaths - Update existing patches to handle roles with multiple base paths - Update bootstrap data to reflect the new base_paths field * feat(role): Restrict modifications to default roles (admin and guest) - Add validation to prevent changes to "admin" and "guest" roles in `UpdateRole` and `DeleteRole` functions. - Introduce `ErrChangeDefaultRole` error in `internal/errs/role.go` to standardize error messaging. - Update role-related API handlers in `server/handles/role.go` to enforce the new restriction. - Enhance comments in `internal/bootstrap/data/role.go` to clarify the significance of default roles. - Ensure consistent error responses for unauthorized role modifications across the application. * 🔄 **refactor(role): Enhance role permission handling** - Replaced `BasePaths` with `PermissionPaths` in `Role` struct for better permission granularity. - Introduced JSON serialization for `PermissionPaths` using `RawPermission` field in `Role` struct. - Implemented `BeforeSave` and `AfterFind` GORM hooks for handling `PermissionPaths` serialization. - Refactored permission calculation logic in `role_perm.go` to work with `PermissionPaths`. - Updated role creation logic to initialize `PermissionPaths` for `admin` and `guest` roles. - Removed deprecated `CheckPathLimit` method from `Role` struct. * fix(model/user/role): update permission settings for admin and role - Change `RawPermission` field in `role.go` to hide JSON representation - Update `Permission` field in `user.go` to `0xFFFF` for full access - Modify `PermissionScopes` in `role.go` to `0xFFFF` for enhanced permissions * 🔒 feat(role-permissions): Enhance role-based access control - Introduce `canReadPathByRole` function in `role_perm.go` to verify path access based on user roles - Modify `CanAccessWithRoles` to include role-based path read check - Add `RoleNames` and `Permissions` to `UserResp` struct in `auth.go` for enhanced user role and permission details - Implement role details aggregation in `auth.go` to populate `RoleNames` and `Permissions` - Update `User` struct in `user.go` to include `RolesDetail` for more detailed role information - Enhance middleware in `auth.go` to load and verify detailed role information for users - Move `guest` user initialization logic in `user.go` to improve code organization and avoid repetition * 🔒 fix(permissions): Add permission checks for archive operations - Add `MergeRolePermissions` and `HasPermission` checks to validate user access for reading archives - Ensure users have `PermReadArchives` before proceeding with `GetNearestMeta` in specific archive paths - Implement permission checks for decompress operations, requiring `PermDecompress` for source paths - Return `PermissionDenied` errors with 403 status if user lacks necessary permissions * 🔒 fix(server): Add permission check for offline download - Add permission merging logic for user roles - Check user has permission for offline download addition - Return error response with "permission denied" if check fails * ✨ feat(role-permission): Implement path-based role permission checks - Add `CheckPathLimitWithRoles` function to validate access based on `PermPathLimit` permission. - Integrate `CheckPathLimitWithRoles` in `offline_download` to enforce path-based access control. - Apply `CheckPathLimitWithRoles` across file system management operations (e.g., creation, movement, deletion). - Ensure `CheckPathLimitWithRoles` is invoked for batch operations and archive-related actions. - Update error handling to return `PermissionDenied` if the path validation fails. - Import `errs` package in `offline_download` for consistent error responses. * ✨ feat(role-permission): Implement path-based role permission checks - Add `CheckPathLimitWithRoles` function to validate access based on `PermPathLimit` permission. - Integrate `CheckPathLimitWithRoles` in `offline_download` to enforce path-based access control. - Apply `CheckPathLimitWithRoles` across file system management operations (e.g., creation, movement, deletion). - Ensure `CheckPathLimitWithRoles` is invoked for batch operations and archive-related actions. - Update error handling to return `PermissionDenied` if the path validation fails. - Import `errs` package in `offline_download` for consistent error responses. * ♻️ refactor(access-control): Update access control logic to use role-based checks - Remove deprecated logic from `CanAccess` function in `check.go`, replacing it with `CanAccessWithRoles` for improved role-based access control. - Modify calls in `search.go` to use `CanAccessWithRoles` for more precise handling of permissions. - Update `fsread.go` to utilize `CanAccessWithRoles`, ensuring accurate access validation based on user roles. - Simplify import statements in `check.go` by removing unused packages to clean up the codebase. * ✨ feat(fs): Improve visibility logic for hidden files - Import `server/common` package to handle permissions more robustly - Update `whetherHide` function to use `MergeRolePermissions` for user-specific path permissions - Replace direct user checks with `HasPermission` for `PermSeeHides` - Enhance logic to ensure `nil` user cases are handled explicitly * 标签管理 * feat(db/auth/user): Enhance role handling and clean permission paths - Comment out role modification checks in `server/handles/user.go` to allow flexible role changes. - Improve permission path handling in `server/handles/auth.go` by normalizing and deduplicating paths. - Introduce `addedPaths` map in `CurrentUser` to prevent duplicate permissions. * feat(storage/db): Implement role permissions path prefix update - Add `UpdateRolePermissionsPathPrefix` function in `role.go` to update role permissions paths. - Modify `storage.go` to call the new function when the mount path is renamed. - Introduce path cleaning and prefix matching logic for accurate path updates. - Ensure roles are updated only if their permission scopes are modified. - Handle potential errors with informative messages during database operations. * feat(role-migration): Implement role conversion and introduce NEWGENERAL role - Add `NEWGENERAL` to the roles enumeration in `user.go` - Create new file `convert_role.go` for migrating legacy roles to new model - Implement `ConvertLegacyRoles` function to handle role conversion with permission scopes - Add `convert_role.go` patch to `all.go` under version `v3.46.0` * feat(role/auth): Add role retrieval by user ID and update path prefixes - Add `GetRolesByUserID` function for efficient role retrieval by user ID - Implement `UpdateUserBasePathPrefix` to update user base paths - Modify `UpdateRolePermissionsPathPrefix` to return modified role IDs - Update `auth.go` middleware to use the new role retrieval function - Refresh role and user caches upon path prefix updates to maintain consistency --------- Co-authored-by: Leslie-Xy <540049476@qq.com>
445 lines
13 KiB
Go
445 lines
13 KiB
Go
package handles
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Xhofe/go-cache"
|
|
|
|
"github.com/alist-org/alist/v3/internal/conf"
|
|
"github.com/alist-org/alist/v3/internal/db"
|
|
"github.com/alist-org/alist/v3/internal/model"
|
|
"github.com/alist-org/alist/v3/internal/setting"
|
|
"github.com/alist-org/alist/v3/pkg/utils"
|
|
"github.com/alist-org/alist/v3/pkg/utils/random"
|
|
"github.com/alist-org/alist/v3/server/common"
|
|
"github.com/coreos/go-oidc"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/go-resty/resty/v2"
|
|
"golang.org/x/oauth2"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
const stateLength = 16
|
|
const stateExpire = time.Minute * 5
|
|
|
|
var stateCache = cache.NewMemCache[string](cache.WithShards[string](stateLength))
|
|
|
|
func _keyState(clientID, state string) string {
|
|
return fmt.Sprintf("%s_%s", clientID, state)
|
|
}
|
|
|
|
func generateState(clientID, ip string) string {
|
|
state := random.String(stateLength)
|
|
stateCache.Set(_keyState(clientID, state), ip, cache.WithEx[string](stateExpire))
|
|
return state
|
|
}
|
|
|
|
func verifyState(clientID, ip, state string) bool {
|
|
value, ok := stateCache.Get(_keyState(clientID, state))
|
|
return ok && value == ip
|
|
}
|
|
|
|
func ssoRedirectUri(c *gin.Context, useCompatibility bool, method string) string {
|
|
if useCompatibility {
|
|
return common.GetApiUrl(c.Request) + "/api/auth/" + method
|
|
} else {
|
|
return common.GetApiUrl(c.Request) + "/api/auth/sso_callback" + "?method=" + method
|
|
}
|
|
}
|
|
|
|
func SSOLoginRedirect(c *gin.Context) {
|
|
method := c.Query("method")
|
|
useCompatibility := setting.GetBool(conf.SSOCompatibilityMode)
|
|
enabled := setting.GetBool(conf.SSOLoginEnabled)
|
|
clientId := setting.GetStr(conf.SSOClientId)
|
|
platform := setting.GetStr(conf.SSOLoginPlatform)
|
|
var rUrl string
|
|
if !enabled {
|
|
common.ErrorStrResp(c, "Single sign-on is not enabled", 403)
|
|
return
|
|
}
|
|
urlValues := url.Values{}
|
|
if method == "" {
|
|
common.ErrorStrResp(c, "no method provided", 400)
|
|
return
|
|
}
|
|
redirectUri := ssoRedirectUri(c, useCompatibility, method)
|
|
urlValues.Add("response_type", "code")
|
|
urlValues.Add("redirect_uri", redirectUri)
|
|
urlValues.Add("client_id", clientId)
|
|
switch platform {
|
|
case "Github":
|
|
rUrl = "https://github.com/login/oauth/authorize?"
|
|
urlValues.Add("scope", "read:user")
|
|
case "Microsoft":
|
|
rUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?"
|
|
urlValues.Add("scope", "user.read")
|
|
urlValues.Add("response_mode", "query")
|
|
case "Google":
|
|
rUrl = "https://accounts.google.com/o/oauth2/v2/auth?"
|
|
urlValues.Add("scope", "https://www.googleapis.com/auth/userinfo.profile")
|
|
case "Dingtalk":
|
|
rUrl = "https://login.dingtalk.com/oauth2/auth?"
|
|
urlValues.Add("scope", "openid")
|
|
urlValues.Add("prompt", "consent")
|
|
urlValues.Add("response_type", "code")
|
|
case "Casdoor":
|
|
endpoint := strings.TrimSuffix(setting.GetStr(conf.SSOEndpointName), "/")
|
|
rUrl = endpoint + "/login/oauth/authorize?"
|
|
urlValues.Add("scope", "profile")
|
|
urlValues.Add("state", endpoint)
|
|
case "OIDC":
|
|
oauth2Config, err := GetOIDCClient(c, useCompatibility, redirectUri, method)
|
|
if err != nil {
|
|
common.ErrorStrResp(c, err.Error(), 400)
|
|
return
|
|
}
|
|
state := generateState(clientId, c.ClientIP())
|
|
c.Redirect(http.StatusFound, oauth2Config.AuthCodeURL(state))
|
|
return
|
|
default:
|
|
common.ErrorStrResp(c, "invalid platform", 400)
|
|
return
|
|
}
|
|
c.Redirect(302, rUrl+urlValues.Encode())
|
|
}
|
|
|
|
var ssoClient = resty.New().SetRetryCount(3)
|
|
|
|
func GetOIDCClient(c *gin.Context, useCompatibility bool, redirectUri, method string) (*oauth2.Config, error) {
|
|
if redirectUri == "" {
|
|
redirectUri = ssoRedirectUri(c, useCompatibility, method)
|
|
}
|
|
endpoint := setting.GetStr(conf.SSOEndpointName)
|
|
provider, err := oidc.NewProvider(c, endpoint)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
clientId := setting.GetStr(conf.SSOClientId)
|
|
clientSecret := setting.GetStr(conf.SSOClientSecret)
|
|
extraScopes := []string{}
|
|
if setting.GetStr(conf.SSOExtraScopes) != "" {
|
|
extraScopes = strings.Split(setting.GetStr(conf.SSOExtraScopes), " ")
|
|
}
|
|
return &oauth2.Config{
|
|
ClientID: clientId,
|
|
ClientSecret: clientSecret,
|
|
RedirectURL: redirectUri,
|
|
|
|
// Discovery returns the OAuth2 endpoints.
|
|
Endpoint: provider.Endpoint(),
|
|
|
|
// "openid" is a required scope for OpenID Connect flows.
|
|
Scopes: append([]string{oidc.ScopeOpenID, "profile"}, extraScopes...),
|
|
}, nil
|
|
}
|
|
|
|
func autoRegister(username, userID string, err error) (*model.User, error) {
|
|
if !errors.Is(err, gorm.ErrRecordNotFound) || !setting.GetBool(conf.SSOAutoRegister) {
|
|
return nil, err
|
|
}
|
|
if username == "" {
|
|
return nil, errors.New("cannot get username from SSO provider")
|
|
}
|
|
user := &model.User{
|
|
ID: 0,
|
|
Username: username,
|
|
Password: random.String(16),
|
|
Permission: int32(setting.GetInt(conf.SSODefaultPermission, 0)),
|
|
BasePath: setting.GetStr(conf.SSODefaultDir),
|
|
Role: nil,
|
|
Disabled: false,
|
|
SsoID: userID,
|
|
}
|
|
if err = db.CreateUser(user); err != nil {
|
|
if strings.HasPrefix(err.Error(), "UNIQUE constraint failed") && strings.HasSuffix(err.Error(), "username") {
|
|
user.Username = user.Username + "_" + userID
|
|
if err = db.CreateUser(user); err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
return nil, err
|
|
}
|
|
}
|
|
return user, nil
|
|
}
|
|
|
|
func parseJWT(p string) ([]byte, error) {
|
|
parts := strings.Split(p, ".")
|
|
if len(parts) < 2 {
|
|
return nil, fmt.Errorf("oidc: malformed jwt, expected 3 parts got %d", len(parts))
|
|
}
|
|
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("oidc: malformed jwt payload: %v", err)
|
|
}
|
|
return payload, nil
|
|
}
|
|
|
|
func OIDCLoginCallback(c *gin.Context) {
|
|
useCompatibility := setting.GetBool(conf.SSOCompatibilityMode)
|
|
method := c.Query("method")
|
|
if useCompatibility {
|
|
method = path.Base(c.Request.URL.Path)
|
|
}
|
|
clientId := setting.GetStr(conf.SSOClientId)
|
|
endpoint := setting.GetStr(conf.SSOEndpointName)
|
|
provider, err := oidc.NewProvider(c, endpoint)
|
|
if err != nil {
|
|
common.ErrorResp(c, err, 400)
|
|
return
|
|
}
|
|
oauth2Config, err := GetOIDCClient(c, useCompatibility, "", method)
|
|
if err != nil {
|
|
common.ErrorResp(c, err, 400)
|
|
return
|
|
}
|
|
if !verifyState(clientId, c.ClientIP(), c.Query("state")) {
|
|
common.ErrorStrResp(c, "incorrect or expired state parameter", 400)
|
|
return
|
|
}
|
|
|
|
oauth2Token, err := oauth2Config.Exchange(c, c.Query("code"))
|
|
if err != nil {
|
|
common.ErrorResp(c, err, 400)
|
|
return
|
|
}
|
|
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
|
|
if !ok {
|
|
common.ErrorStrResp(c, "no id_token found in oauth2 token", 400)
|
|
return
|
|
}
|
|
verifier := provider.Verifier(&oidc.Config{
|
|
ClientID: clientId,
|
|
})
|
|
_, err = verifier.Verify(c, rawIDToken)
|
|
if err != nil {
|
|
common.ErrorResp(c, err, 400)
|
|
return
|
|
}
|
|
payload, err := parseJWT(rawIDToken)
|
|
if err != nil {
|
|
common.ErrorResp(c, err, 400)
|
|
return
|
|
}
|
|
userID := utils.Json.Get(payload, setting.GetStr(conf.SSOOIDCUsernameKey, "name")).ToString()
|
|
if userID == "" {
|
|
common.ErrorStrResp(c, "cannot get username from OIDC provider", 400)
|
|
return
|
|
}
|
|
if method == "get_sso_id" {
|
|
if useCompatibility {
|
|
c.Redirect(302, common.GetApiUrl(c.Request)+"/@manage?sso_id="+userID)
|
|
return
|
|
}
|
|
html := fmt.Sprintf(`<!DOCTYPE html>
|
|
<head></head>
|
|
<body>
|
|
<script>
|
|
window.opener.postMessage({"sso_id": "%s"}, "*")
|
|
window.close()
|
|
</script>
|
|
</body>`, userID)
|
|
c.Data(200, "text/html; charset=utf-8", []byte(html))
|
|
return
|
|
}
|
|
if method == "sso_get_token" {
|
|
user, err := db.GetUserBySSOID(userID)
|
|
if err != nil {
|
|
user, err = autoRegister(userID, userID, err)
|
|
if err != nil {
|
|
common.ErrorResp(c, err, 400)
|
|
}
|
|
}
|
|
token, err := common.GenerateToken(user)
|
|
if err != nil {
|
|
common.ErrorResp(c, err, 400)
|
|
}
|
|
if useCompatibility {
|
|
c.Redirect(302, common.GetApiUrl(c.Request)+"/@login?token="+token)
|
|
return
|
|
}
|
|
html := fmt.Sprintf(`<!DOCTYPE html>
|
|
<head></head>
|
|
<body>
|
|
<script>
|
|
window.opener.postMessage({"token":"%s"}, "*")
|
|
window.close()
|
|
</script>
|
|
</body>`, token)
|
|
c.Data(200, "text/html; charset=utf-8", []byte(html))
|
|
return
|
|
}
|
|
}
|
|
|
|
func SSOLoginCallback(c *gin.Context) {
|
|
enabled := setting.GetBool(conf.SSOLoginEnabled)
|
|
usecompatibility := setting.GetBool(conf.SSOCompatibilityMode)
|
|
if !enabled {
|
|
common.ErrorResp(c, errors.New("sso login is disabled"), 500)
|
|
return
|
|
}
|
|
argument := c.Query("method")
|
|
if usecompatibility {
|
|
argument = path.Base(c.Request.URL.Path)
|
|
}
|
|
if !utils.SliceContains([]string{"get_sso_id", "sso_get_token"}, argument) {
|
|
common.ErrorResp(c, errors.New("invalid request"), 500)
|
|
return
|
|
}
|
|
clientId := setting.GetStr(conf.SSOClientId)
|
|
platform := setting.GetStr(conf.SSOLoginPlatform)
|
|
clientSecret := setting.GetStr(conf.SSOClientSecret)
|
|
var tokenUrl, userUrl, scope, authField, idField, usernameField string
|
|
additionalForm := make(map[string]string)
|
|
switch platform {
|
|
case "Github":
|
|
tokenUrl = "https://github.com/login/oauth/access_token"
|
|
userUrl = "https://api.github.com/user"
|
|
authField = "code"
|
|
scope = "read:user"
|
|
idField = "id"
|
|
usernameField = "login"
|
|
case "Microsoft":
|
|
tokenUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/token"
|
|
userUrl = "https://graph.microsoft.com/v1.0/me"
|
|
additionalForm["grant_type"] = "authorization_code"
|
|
scope = "user.read"
|
|
authField = "code"
|
|
idField = "id"
|
|
usernameField = "displayName"
|
|
case "Google":
|
|
tokenUrl = "https://oauth2.googleapis.com/token"
|
|
userUrl = "https://www.googleapis.com/oauth2/v1/userinfo"
|
|
additionalForm["grant_type"] = "authorization_code"
|
|
scope = "https://www.googleapis.com/auth/userinfo.profile"
|
|
authField = "code"
|
|
idField = "id"
|
|
usernameField = "name"
|
|
case "Dingtalk":
|
|
tokenUrl = "https://api.dingtalk.com/v1.0/oauth2/userAccessToken"
|
|
userUrl = "https://api.dingtalk.com/v1.0/contact/users/me"
|
|
authField = "authCode"
|
|
idField = "unionId"
|
|
usernameField = "nick"
|
|
case "Casdoor":
|
|
endpoint := strings.TrimSuffix(setting.GetStr(conf.SSOEndpointName), "/")
|
|
tokenUrl = endpoint + "/api/login/oauth/access_token"
|
|
userUrl = endpoint + "/api/userinfo"
|
|
additionalForm["grant_type"] = "authorization_code"
|
|
scope = "profile"
|
|
authField = "code"
|
|
idField = "sub"
|
|
usernameField = "preferred_username"
|
|
case "OIDC":
|
|
OIDCLoginCallback(c)
|
|
return
|
|
default:
|
|
common.ErrorStrResp(c, "invalid platform", 400)
|
|
return
|
|
}
|
|
callbackCode := c.Query(authField)
|
|
if callbackCode == "" {
|
|
common.ErrorStrResp(c, "No code provided", 400)
|
|
return
|
|
}
|
|
var resp *resty.Response
|
|
var err error
|
|
if platform == "Dingtalk" {
|
|
resp, err = ssoClient.R().SetHeader("content-type", "application/json").SetHeader("Accept", "application/json").
|
|
SetBody(map[string]string{
|
|
"clientId": clientId,
|
|
"clientSecret": clientSecret,
|
|
"code": callbackCode,
|
|
"grantType": "authorization_code",
|
|
}).
|
|
Post(tokenUrl)
|
|
} else {
|
|
var redirect_uri string
|
|
if usecompatibility {
|
|
redirect_uri = common.GetApiUrl(c.Request) + "/api/auth/" + argument
|
|
} else {
|
|
redirect_uri = common.GetApiUrl(c.Request) + "/api/auth/sso_callback" + "?method=" + argument
|
|
}
|
|
resp, err = ssoClient.R().SetHeader("Accept", "application/json").
|
|
SetFormData(map[string]string{
|
|
"client_id": clientId,
|
|
"client_secret": clientSecret,
|
|
"code": callbackCode,
|
|
"redirect_uri": redirect_uri,
|
|
"scope": scope,
|
|
}).SetFormData(additionalForm).Post(tokenUrl)
|
|
}
|
|
if err != nil {
|
|
common.ErrorResp(c, err, 400)
|
|
return
|
|
}
|
|
if platform == "Dingtalk" {
|
|
accessToken := utils.Json.Get(resp.Body(), "accessToken").ToString()
|
|
resp, err = ssoClient.R().SetHeader("x-acs-dingtalk-access-token", accessToken).
|
|
Get(userUrl)
|
|
} else {
|
|
accessToken := utils.Json.Get(resp.Body(), "access_token").ToString()
|
|
resp, err = ssoClient.R().SetHeader("Authorization", "Bearer "+accessToken).
|
|
Get(userUrl)
|
|
}
|
|
if err != nil {
|
|
common.ErrorResp(c, err, 400)
|
|
return
|
|
}
|
|
userID := utils.Json.Get(resp.Body(), idField).ToString()
|
|
if utils.SliceContains([]string{"", "0"}, userID) {
|
|
common.ErrorResp(c, errors.New("error occurred"), 400)
|
|
return
|
|
}
|
|
if argument == "get_sso_id" {
|
|
if usecompatibility {
|
|
c.Redirect(302, common.GetApiUrl(c.Request)+"/@manage?sso_id="+userID)
|
|
return
|
|
}
|
|
html := fmt.Sprintf(`<!DOCTYPE html>
|
|
<head></head>
|
|
<body>
|
|
<script>
|
|
window.opener.postMessage({"sso_id": "%s"}, "*")
|
|
window.close()
|
|
</script>
|
|
</body>`, userID)
|
|
c.Data(200, "text/html; charset=utf-8", []byte(html))
|
|
return
|
|
}
|
|
username := utils.Json.Get(resp.Body(), usernameField).ToString()
|
|
user, err := db.GetUserBySSOID(userID)
|
|
if err != nil {
|
|
user, err = autoRegister(username, userID, err)
|
|
if err != nil {
|
|
common.ErrorResp(c, err, 400)
|
|
return
|
|
}
|
|
}
|
|
token, err := common.GenerateToken(user)
|
|
if err != nil {
|
|
common.ErrorResp(c, err, 400)
|
|
}
|
|
if usecompatibility {
|
|
c.Redirect(302, common.GetApiUrl(c.Request)+"/@login?token="+token)
|
|
return
|
|
}
|
|
html := fmt.Sprintf(`<!DOCTYPE html>
|
|
<head></head>
|
|
<body>
|
|
<script>
|
|
window.opener.postMessage({"token":"%s"}, "*")
|
|
window.close()
|
|
</script>
|
|
</body>`, token)
|
|
c.Data(200, "text/html; charset=utf-8", []byte(html))
|
|
}
|