forked from casdoor/casdoor
311 lines
9.4 KiB
Go
311 lines
9.4 KiB
Go
// Copyright 2025 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.
|
|
|
|
package object
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/casdoor/casdoor/util"
|
|
)
|
|
|
|
// OktaSyncerProvider implements SyncerProvider for Okta API-based syncers
|
|
type OktaSyncerProvider struct {
|
|
Syncer *Syncer
|
|
}
|
|
|
|
// InitAdapter initializes the Okta syncer (no database adapter needed)
|
|
func (p *OktaSyncerProvider) InitAdapter() error {
|
|
// Okta syncer doesn't need database adapter
|
|
return nil
|
|
}
|
|
|
|
// GetOriginalUsers retrieves all users from Okta API
|
|
func (p *OktaSyncerProvider) GetOriginalUsers() ([]*OriginalUser, error) {
|
|
return p.getOktaOriginalUsers()
|
|
}
|
|
|
|
// AddUser adds a new user to Okta (not supported for read-only API)
|
|
func (p *OktaSyncerProvider) AddUser(user *OriginalUser) (bool, error) {
|
|
// Okta syncer is typically read-only
|
|
return false, fmt.Errorf("adding users to Okta is not supported")
|
|
}
|
|
|
|
// UpdateUser updates an existing user in Okta (not supported for read-only API)
|
|
func (p *OktaSyncerProvider) UpdateUser(user *OriginalUser) (bool, error) {
|
|
// Okta syncer is typically read-only
|
|
return false, fmt.Errorf("updating users in Okta is not supported")
|
|
}
|
|
|
|
// TestConnection tests the Okta API connection
|
|
func (p *OktaSyncerProvider) TestConnection() error {
|
|
// Try to fetch first page of users to verify connection
|
|
_, _, err := p.getOktaUsers("")
|
|
return err
|
|
}
|
|
|
|
// Close closes any open connections (no-op for Okta API-based syncer)
|
|
func (p *OktaSyncerProvider) Close() error {
|
|
// Okta syncer doesn't maintain persistent connections
|
|
return nil
|
|
}
|
|
|
|
// OktaUser represents a user from Okta API
|
|
type OktaUser struct {
|
|
Id string `json:"id"`
|
|
Status string `json:"status"`
|
|
Created string `json:"created"`
|
|
Profile struct {
|
|
Login string `json:"login"`
|
|
Email string `json:"email"`
|
|
FirstName string `json:"firstName"`
|
|
LastName string `json:"lastName"`
|
|
DisplayName string `json:"displayName"`
|
|
MobilePhone string `json:"mobilePhone"`
|
|
PrimaryPhone string `json:"primaryPhone"`
|
|
StreetAddress string `json:"streetAddress"`
|
|
City string `json:"city"`
|
|
State string `json:"state"`
|
|
ZipCode string `json:"zipCode"`
|
|
CountryCode string `json:"countryCode"`
|
|
PostalAddress string `json:"postalAddress"`
|
|
PreferredLanguage string `json:"preferredLanguage"`
|
|
Locale string `json:"locale"`
|
|
Timezone string `json:"timezone"`
|
|
Title string `json:"title"`
|
|
Department string `json:"department"`
|
|
Organization string `json:"organization"`
|
|
} `json:"profile"`
|
|
}
|
|
|
|
// parseLinkHeader parses the HTTP Link header
|
|
// Format: <https://example.com/api/v1/users?after=xyz>; rel="next"
|
|
func parseLinkHeader(header string) map[string]string {
|
|
links := make(map[string]string)
|
|
parts := strings.Split(header, ",")
|
|
for _, part := range parts {
|
|
section := strings.Split(strings.TrimSpace(part), ";")
|
|
if len(section) != 2 {
|
|
continue
|
|
}
|
|
|
|
url := strings.Trim(strings.TrimSpace(section[0]), "<>")
|
|
rel := strings.TrimSpace(section[1])
|
|
|
|
if strings.HasPrefix(rel, "rel=\"") && strings.HasSuffix(rel, "\"") {
|
|
relValue := rel[5 : len(rel)-1]
|
|
links[relValue] = url
|
|
}
|
|
}
|
|
return links
|
|
}
|
|
|
|
// getOktaUsers retrieves users from Okta API with pagination support
|
|
// Returns users and the next page link (if any)
|
|
func (p *OktaSyncerProvider) getOktaUsers(nextLink string) ([]*OktaUser, string, error) {
|
|
// syncer.Host should be the Okta domain (e.g., "dev-12345.okta.com" or full URL)
|
|
// syncer.Password should be the API token
|
|
|
|
domain := p.Syncer.Host
|
|
if domain == "" {
|
|
return nil, "", fmt.Errorf("Okta domain (host field) is required for Okta syncer")
|
|
}
|
|
|
|
apiToken := p.Syncer.Password
|
|
if apiToken == "" {
|
|
return nil, "", fmt.Errorf("API token (password field) is required for Okta syncer")
|
|
}
|
|
|
|
// Construct API URL
|
|
var apiUrl string
|
|
if nextLink != "" {
|
|
apiUrl = nextLink
|
|
} else {
|
|
// Remove https:// or http:// prefix if present in domain
|
|
domain = strings.TrimPrefix(strings.TrimPrefix(domain, "https://"), "http://")
|
|
apiUrl = fmt.Sprintf("https://%s/api/v1/users?limit=200", domain)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "GET", apiUrl, nil)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
req.Header.Set("Authorization", "SSWS "+apiToken)
|
|
req.Header.Set("Accept", "application/json")
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
client := &http.Client{Timeout: 30 * time.Second}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return nil, "", fmt.Errorf("failed to get users from Okta: status=%d, body=%s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
var users []*OktaUser
|
|
err = json.Unmarshal(body, &users)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
// Parse Link header for next page
|
|
// Link header format: <https://...>; rel="next"
|
|
nextPageLink := ""
|
|
linkHeader := resp.Header.Get("Link")
|
|
if linkHeader != "" {
|
|
links := parseLinkHeader(linkHeader)
|
|
if next, ok := links["next"]; ok {
|
|
nextPageLink = next
|
|
}
|
|
}
|
|
|
|
return users, nextPageLink, nil
|
|
}
|
|
|
|
// oktaUserToOriginalUser converts Okta user to Casdoor OriginalUser
|
|
func (p *OktaSyncerProvider) oktaUserToOriginalUser(oktaUser *OktaUser) *OriginalUser {
|
|
user := &OriginalUser{
|
|
Id: oktaUser.Id,
|
|
Name: oktaUser.Profile.Login,
|
|
DisplayName: oktaUser.Profile.DisplayName,
|
|
FirstName: oktaUser.Profile.FirstName,
|
|
LastName: oktaUser.Profile.LastName,
|
|
Email: oktaUser.Profile.Email,
|
|
Phone: oktaUser.Profile.MobilePhone,
|
|
Title: oktaUser.Profile.Title,
|
|
Language: oktaUser.Profile.PreferredLanguage,
|
|
Address: []string{},
|
|
Properties: map[string]string{},
|
|
Groups: []string{},
|
|
}
|
|
|
|
// Build address from street, city, state, zip
|
|
if oktaUser.Profile.StreetAddress != "" {
|
|
user.Address = append(user.Address, oktaUser.Profile.StreetAddress)
|
|
}
|
|
if oktaUser.Profile.City != "" {
|
|
user.Address = append(user.Address, oktaUser.Profile.City)
|
|
}
|
|
if oktaUser.Profile.State != "" {
|
|
user.Address = append(user.Address, oktaUser.Profile.State)
|
|
}
|
|
if oktaUser.Profile.ZipCode != "" {
|
|
user.Address = append(user.Address, oktaUser.Profile.ZipCode)
|
|
}
|
|
|
|
// Store additional properties
|
|
if oktaUser.Profile.Department != "" {
|
|
user.Properties["department"] = oktaUser.Profile.Department
|
|
}
|
|
if oktaUser.Profile.Organization != "" {
|
|
user.Properties["organization"] = oktaUser.Profile.Organization
|
|
}
|
|
if oktaUser.Profile.Timezone != "" {
|
|
user.Properties["timezone"] = oktaUser.Profile.Timezone
|
|
}
|
|
|
|
// Set IsForbidden based on status
|
|
// Okta status values: STAGED, PROVISIONED, ACTIVE, RECOVERY, PASSWORD_EXPIRED, LOCKED_OUT, SUSPENDED, DEPROVISIONED
|
|
if oktaUser.Status == "SUSPENDED" || oktaUser.Status == "DEPROVISIONED" || oktaUser.Status == "LOCKED_OUT" {
|
|
user.IsForbidden = true
|
|
} else {
|
|
user.IsForbidden = false
|
|
}
|
|
|
|
// If display name is empty, construct from first and last name
|
|
if user.DisplayName == "" && (user.FirstName != "" || user.LastName != "") {
|
|
user.DisplayName = strings.TrimSpace(fmt.Sprintf("%s %s", user.FirstName, user.LastName))
|
|
}
|
|
|
|
// If email is empty, use login as email (typically login is an email)
|
|
if user.Email == "" && oktaUser.Profile.Login != "" {
|
|
user.Email = oktaUser.Profile.Login
|
|
}
|
|
|
|
// If mobile phone is empty, try primary phone
|
|
if user.Phone == "" && oktaUser.Profile.PrimaryPhone != "" {
|
|
user.Phone = oktaUser.Profile.PrimaryPhone
|
|
}
|
|
|
|
// Set CreatedTime to current time if not set
|
|
if user.CreatedTime == "" {
|
|
user.CreatedTime = util.GetCurrentTime()
|
|
}
|
|
|
|
return user
|
|
}
|
|
|
|
// getOktaOriginalUsers is the main entry point for Okta syncer
|
|
func (p *OktaSyncerProvider) getOktaOriginalUsers() ([]*OriginalUser, error) {
|
|
allUsers := []*OktaUser{}
|
|
nextLink := ""
|
|
|
|
// Fetch all users with pagination
|
|
for {
|
|
users, next, err := p.getOktaUsers(nextLink)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
allUsers = append(allUsers, users...)
|
|
|
|
// If there's no next link, we've fetched all users
|
|
if next == "" {
|
|
break
|
|
}
|
|
|
|
nextLink = next
|
|
}
|
|
|
|
// Convert Okta users to Casdoor OriginalUser
|
|
originalUsers := []*OriginalUser{}
|
|
for _, oktaUser := range allUsers {
|
|
originalUser := p.oktaUserToOriginalUser(oktaUser)
|
|
originalUsers = append(originalUsers, originalUser)
|
|
}
|
|
|
|
return originalUsers, nil
|
|
}
|
|
|
|
// GetOriginalGroups retrieves all groups from Okta (not implemented yet)
|
|
func (p *OktaSyncerProvider) GetOriginalGroups() ([]*OriginalGroup, error) {
|
|
// TODO: Implement Okta group sync
|
|
return []*OriginalGroup{}, nil
|
|
}
|
|
|
|
// GetOriginalUserGroups retrieves the group IDs that a user belongs to (not implemented yet)
|
|
func (p *OktaSyncerProvider) GetOriginalUserGroups(userId string) ([]string, error) {
|
|
// TODO: Implement Okta user group membership sync
|
|
return []string{}, nil
|
|
}
|