Merge branch 'pam' of github.com:jumpserver/jumpserver into pam

pull/14556/head
ibuler 2024-12-02 10:35:38 +08:00
commit c8a632ed60
45 changed files with 1817 additions and 16 deletions

View File

@ -2,3 +2,4 @@ from .account import *
from .task import *
from .template import *
from .virtual import *
from .service import *

View File

@ -12,6 +12,7 @@ from assets.models import Asset, Node
from authentication.permissions import UserConfirmation, ConfirmType
from common.api.mixin import ExtraFilterFieldsMixin
from common.permissions import IsValidUser
from common.drf.filters import AttrRulesFilterBackend
from orgs.mixins.api import OrgBulkModelViewSet
from rbac.permissions import RBACPermission
@ -24,6 +25,7 @@ __all__ = [
class AccountViewSet(OrgBulkModelViewSet):
model = Account
search_fields = ('username', 'name', 'asset__name', 'asset__address', 'comment')
extra_filter_backends = [AttrRulesFilterBackend]
filterset_class = AccountFilterSet
serializer_classes = {
'default': serializers.AccountSerializer,

View File

@ -0,0 +1,74 @@
import os
from django.utils.translation import gettext_lazy as _
from django.conf import settings
from django.utils import translation
from rest_framework.decorators import action
from rest_framework.response import Response
from accounts import serializers
from accounts.models import ServiceIntegration
from audits.models import ServiceAccessLog
from authentication.permissions import UserConfirmation, ConfirmType
from common.exceptions import JMSException
from common.permissions import IsValidUser
from common.utils import get_request_ip
from orgs.mixins.api import OrgBulkModelViewSet
from rbac.permissions import RBACPermission
class ServiceIntegrationViewSet(OrgBulkModelViewSet):
model = ServiceIntegration
search_fields = ('name', 'comment')
serializer_classes = {
'default': serializers.ServiceIntegrationSerializer,
'get_account_secret': serializers.ServiceAccountSecretSerializer
}
rbac_perms = {
'get_once_secret': 'accounts.change_serviceintegration',
'get_account_secret': 'accounts.view_serviceintegration',
}
@action(
['GET'], detail=False, url_path='sdks',
permission_classes=[IsValidUser]
)
def get_sdks_info(self, request, *args, **kwargs):
readme = ''
sdk_language = self.request.query_params.get('language', 'python')
filename = f'readme.{translation.get_language()}.md'
readme_path = os.path.join(
settings.APPS_DIR, 'accounts', 'demos', sdk_language, filename
)
if os.path.exists(readme_path):
with open(readme_path, 'r') as f:
readme = f.read()
return Response(data={'readme': readme })
@action(
['GET'], detail=True, url_path='secret',
permission_classes=[RBACPermission, UserConfirmation.require(ConfirmType.MFA)]
)
def get_once_secret(self, request, *args, **kwargs):
instance = self.get_object()
secret = instance.get_secret()
return Response(data={'id': instance.id, 'secret': secret})
@action(['GET'], detail=False, url_path='account-secret')
def get_account_secret(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.query_params)
if not serializer.is_valid():
return Response({'error': serializer.errors}, status=400)
service = request.user
account = service.get_account(**serializer.data)
if not account:
msg = _('Account not found')
raise JMSException(code='Not found', detail='%s' % msg)
asset = account.asset
ServiceAccessLog.objects.create(
remote_addr=get_request_ip(request), service=service.name, service_id=service.id,
account=f'{account.name}({account.username})', asset=f'{asset.name}({asset.address})',
)
return Response(data={'id': request.user.id, 'secret': account.secret})

View File

@ -1,6 +1,7 @@
from .backup import *
from .base import *
from .change_secret import *
from .change_secret_dashboard import *
from .check_account import *
from .gather_account import *
from .push_account import *

View File

@ -54,7 +54,10 @@ class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet):
return super().get_permissions()
def get_queryset(self):
return ChangeSecretRecord.objects.all()
qs = ChangeSecretRecord.get_valid_records()
return qs.objects.filter(
execution__automation__type=self.tp
)
@action(methods=['post'], detail=False, url_path='execute')
def execute(self, request, *args, **kwargs):

View File

@ -0,0 +1,170 @@
# -*- coding: utf-8 -*-
#
from collections import defaultdict
from django.http.response import JsonResponse
from django.utils import timezone
from rest_framework.views import APIView
from accounts.const import AutomationTypes, ChangeSecretRecordStatusChoice
from accounts.models import ChangeSecretAutomation, AutomationExecution, ChangeSecretRecord
from assets.models import Node, Asset
from common.utils import lazyproperty
from common.utils.timezone import local_zero_hour, local_now
from ops.celery import app
__all__ = ['ChangeSecretDashboardApi']
class ChangeSecretDashboardApi(APIView):
http_method_names = ['get']
rbac_perms = {
'GET': 'accounts.view_changesecretautomation',
}
tp = AutomationTypes.change_secret
task_name = 'accounts.tasks.automation.execute_account_automation_task'
@lazyproperty
def days(self):
count = self.request.query_params.get('days', 1)
return int(count)
@property
def days_to_datetime(self):
if self.days == 1:
return local_zero_hour()
return local_now() - timezone.timedelta(days=self.days)
def get_queryset_date_filter(self, qs, query_field='date_updated'):
return qs.filter(**{f'{query_field}__gte': self.days_to_datetime})
@lazyproperty
def date_range_list(self):
return [
(local_now() - timezone.timedelta(days=i)).date()
for i in range(self.days - 1, -1, -1)
]
def filter_by_date_range(self, queryset, field_name):
date_range_bounds = self.days_to_datetime.date(), (local_now() + timezone.timedelta(days=1)).date()
return queryset.filter(**{f'{field_name}__range': date_range_bounds})
def calculate_daily_metrics(self, queryset, date_field):
filtered_queryset = self.filter_by_date_range(queryset, date_field)
results = filtered_queryset.values_list(date_field, 'status')
status_counts = defaultdict(lambda: defaultdict(int))
for date_finished, status in results:
date_str = str(date_finished.date())
if status == ChangeSecretRecordStatusChoice.failed:
status_counts[date_str]['failed'] += 1
elif status == ChangeSecretRecordStatusChoice.success:
status_counts[date_str]['success'] += 1
metrics = defaultdict(list)
for date in self.date_range_list:
date_str = str(date)
for status in ['success', 'failed']:
metrics[status].append(status_counts[date_str].get(status, 0))
return metrics
def get_daily_success_and_failure_metrics(self):
metrics = self.calculate_daily_metrics(self.change_secret_records_queryset, 'date_finished')
return metrics.get('success', []), metrics.get('failed', [])
@lazyproperty
def change_secrets_queryset(self):
return ChangeSecretAutomation.objects.all()
@lazyproperty
def change_secret_executions_queryset(self):
return AutomationExecution.objects.filter(automation__type=self.tp)
@lazyproperty
def change_secret_records_queryset(self):
return ChangeSecretRecord.get_valid_records().filter(execution__automation__type=self.tp)
def get_change_secret_asset_queryset(self):
qs = self.get_queryset_date_filter(self.change_secrets_queryset)
node_ids = qs.filter(nodes__isnull=False).values_list('nodes', flat=True).distinct()
nodes = Node.objects.filter(id__in=node_ids)
node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True)
direct_asset_ids = qs.filter(assets__isnull=False).values_list('assets', flat=True).distinct()
asset_ids = set(list(direct_asset_ids) + list(node_asset_ids))
return Asset.objects.filter(id__in=asset_ids)
def get_filtered_counts(self, qs, field):
return self.get_queryset_date_filter(qs, field).count()
@staticmethod
def get_status_counts(records):
pending = ChangeSecretRecordStatusChoice.pending
failed = ChangeSecretRecordStatusChoice.failed
total_ids = {str(i) for i in records.exclude(status=pending).values('execution_id').distinct()}
failed_ids = {str(i) for i in records.filter(status=failed).values('execution_id').distinct()}
total = len(total_ids)
failed = len(total_ids & failed_ids)
return {
'total_count_change_secret_executions': total,
'total_count_success_change_secret_executions': total - failed,
'total_count_failed_change_secret_executions': failed,
}
def get(self, request, *args, **kwargs):
query_params = self.request.query_params
data = {}
if query_params.get('total_count_change_secrets'):
data['total_count_change_secrets'] = self.get_filtered_counts(
self.change_secrets_queryset, 'date_updated'
)
if query_params.get('total_count_periodic_change_secrets'):
data['total_count_periodic_change_secrets'] = self.get_filtered_counts(
self.change_secrets_queryset.filter(is_periodic=True), 'date_updated'
)
if query_params.get('total_count_change_secret_assets'):
data['total_count_change_secret_assets'] = self.get_change_secret_asset_queryset().count()
if query_params.get('total_count_change_secret_status'):
records = self.get_queryset_date_filter(self.change_secret_records_queryset, 'date_finished')
data.update(self.get_status_counts(records))
if query_params.get('total_count_change_secret_status'):
records = self.get_queryset_date_filter(self.change_secret_records_queryset, 'date_finished')
data.update(self.get_status_counts(records))
if query_params.get('daily_success_and_failure_metrics'):
success, failed = self.get_daily_success_and_failure_metrics()
data.update({
'dates_metrics_date': [date.strftime('%m-%d') for date in self.date_range_list] or ['0'],
'dates_metrics_total_count_success': success,
'dates_metrics_total_count_failed': failed,
})
if query_params.get('total_count_ongoing_change_secret'):
execution_ids = []
inspect = app.control.inspect()
active_tasks = inspect.active()
for tasks in active_tasks.values():
for task in tasks:
_id = task.get('id')
name = task.get('name')
tp = task.kwargs.get('tp')
if name == self.task_name and tp == self.tp:
execution_ids.append(_id)
snapshots = self.change_secret_executions_queryset.filter(
id__in=execution_ids).values_list('id', 'snapshot')
asset_ids = {asset for i in snapshots for asset in i.get('assets', [])}
account_ids = {account for i in snapshots for account in i.get('accounts', [])}
data['total_count_ongoing_change_secret'] = len(execution_ids)
data['total_count_ongoing_change_secret_assets'] = len(asset_ids)
data['total_count_ongoing_change_secret_accounts'] = len(account_ids)
return JsonResponse(data, status=200)

View File

@ -45,8 +45,9 @@ class PushAccountRecordViewSet(ChangeSecretRecordViewSet):
tp = AutomationTypes.push_account
def get_queryset(self):
return ChangeSecretRecord.objects.filter(
execution__automation__type=AutomationTypes.push_account
qs = ChangeSecretRecord.get_valid_records()
return qs.objects.filter(
execution__automation__type=self.tp
)

View File

@ -0,0 +1,133 @@
# JumpServer PAM Client
This package provides a Go client for interacting with the JumpServer PAM API to retrieve secrets for various assets. It simplifies the process of sending requests and handling responses.
## Features
- Validate parameters before sending requests.
- Support for both asset and account-based secret retrieval.
- Easy integration with JumpServer PAM API using HMAC-SHA256 signatures for authentication.
## Usage Instructions
1. **Download Go Code Files**:
Download the code files into your project directory.
2. **Import the Package**:
Import the package in your Go file, and you can directly use its functionalities.
## Requirements
- `Go 1.16+`
- `github.com/google/uuid`
- `gopkg.in/twindagger/httpsig.v1`
## Usage
### Initialization
To use the JumpServer PAM client, create an instance by providing the required `endpoint`, `keyID`, and `keySecret`.
```go
package main
import (
"fmt"
"your_module_path/jms_pam"
)
func main() {
client := jms_pam.NewJumpServerPAM(
"http://127.0.0.1", // Replace with your JumpServer endpoint
"your-key-id", // Replace with your actual Key ID
"your-key-secret", // Replace with your actual Key Secret
"", // Leave empty for default organization ID
)
}
```
### Creating a Secret Request
You can create a request for a secret by specifying the asset or account information.
```go
request, err := jms_pam.NewSecretRequest("Linux", "", "root", "")
if err != nil {
fmt.Println("Error creating request:", err)
return
}
```
### Sending the Request
Send the request using the `Send` method of the client.
```go
secretObj, err := client.Send(request)
if err != nil {
fmt.Println("Error sending request:", err)
return
}
```
### Handling the Response
Check if the secret was retrieved successfully and handle the response accordingly.
```go
if secretObj.Valid {
fmt.Println("Secret:", secretObj.Secret)
} else {
fmt.Println("Get secret failed:", string(secretObj.Desc))
}
```
### Complete Example
Heres a complete example of how to use the client:
```go
package main
import (
"fmt"
"your_module_path/jms_pam"
)
func main() {
client := jms_pam.NewJumpServerPAM(
"http://127.0.0.1",
"your-key-id",
"your-key-secret",
"",
)
request, err := jms_pam.NewSecretRequest("Linux", "", "root", "")
if err != nil {
fmt.Println("Error creating request:", err)
return
}
secretObj, err := client.Send(request)
if err != nil {
fmt.Println("Error sending request:", err)
return
}
if secretObj.Valid {
fmt.Println("Secret:", secretObj.Secret)
} else {
fmt.Println("Get secret failed:", string(secretObj.Desc))
}
}
```
## Error Handling
The library returns errors for invalid parameters when creating a `SecretRequest`. This includes checks for valid UUIDs and ensuring that required parameters are provided.
## Contributing
Contributions are welcome! Please open an issue or submit a pull request for any enhancements or bug fixes.

View File

@ -0,0 +1,133 @@
# JumpServer PAM クライアント
このパッケージは、JumpServer PAM API と対話し、さまざまな資産のパスワードを取得するための Go クライアントを提供します。リクエストの送信とレスポンスの処理を簡素化します。
## 機能
- リクエスト送信前にパラメータを検証します。
- 資産およびアカウントに基づくパスワード取得をサポートします。
- HMAC-SHA256 署名を使用して認証を行い、JumpServer PAM API との統合を容易にします。
## 使用手順
1. **Go コードファイルのダウンロード**
コードファイルをプロジェクトディレクトリにダウンロードします。
2. **パッケージのインポート**
Go ファイルにパッケージをインポートすると、その機能を直接使用できます。
## 要件
- `Go 1.16+`
- `github.com/google/uuid`
- `gopkg.in/twindagger/httpsig.v1`
## 使用方法
### 初期化
JumpServer PAM クライアントを使用するには、必要な `endpoint`、`keyID`、および `keySecret` を提供してインスタンスを作成します。
```go
package main
import (
"fmt"
"your_module_path/jms_pam"
)
func main() {
client := jms_pam.NewJumpServerPAM(
"http://127.0.0.1", // あなたの JumpServer エンドポイントに置き換えてください
"your-key-id", // 実際の Key ID に置き換えてください
"your-key-secret", // 実際の Key Secret に置き換えてください
"", // デフォルトの組織 ID を使用するには空のままにします
)
}
```
### パスワードリクエストの作成
資産またはアカウント情報を指定してリクエストを作成できます。
```go
request, err := jms_pam.NewSecretRequest("Linux", "", "root", "")
if err != nil {
fmt.Println("リクエスト作成中にエラー:", err)
return
}
```
### リクエストの送信
クライアントの `Send` メソッドを使用してリクエストを送信します。
```go
secretObj, err := client.Send(request)
if err != nil {
fmt.Println("リクエスト送信中にエラー:", err)
return
}
```
### レスポンスの処理
パスワードが正常に取得されたかどうかを確認し、それに応じてレスポンスを処理します。
```go
if secretObj.Valid {
fmt.Println("パスワード:", secretObj.Secret)
} else {
fmt.Println("パスワードの取得に失敗:", string(secretObj.Desc))
}
```
### 完全な例
以下は、クライアントの使用方法に関する完全な例です。
```go
package main
import (
"fmt"
"your_module_path/jms_pam"
)
func main() {
client := jms_pam.NewJumpServerPAM(
"http://127.0.0.1",
"your-key-id",
"your-key-secret",
"",
)
request, err := jms_pam.NewSecretRequest("Linux", "", "root", "")
if err != nil {
fmt.Println("リクエスト作成中にエラー:", err)
return
}
secretObj, err := client.Send(request)
if err != nil {
fmt.Println("リクエスト送信中にエラー:", err)
return
}
if secretObj.Valid {
fmt.Println("パスワード:", secretObj.Secret)
} else {
fmt.Println("パスワードの取得に失敗:", string(secretObj.Desc))
}
}
```
## エラーハンドリング
このライブラリは、`SecretRequest` を作成する際に無効なパラメータに対するエラーを返します。これには、有効な UUID の確認や、必要なパラメータが提供されていることの確認が含まれます。
## 貢献
貢献を歓迎します!改善やバグ修正のために問題を提起したり、プルリクエストを送信してください。

View File

@ -0,0 +1,133 @@
# JumpServer PAM 客户端
该包提供了一个 Go 客户端,用于与 JumpServer PAM API 交互,以检索各种资产的密码。它简化了发送请求和处理响应的过程。
## 功能
- 在发送请求之前验证参数。
- 支持基于资产和账户的密码检索。
- 使用 HMAC-SHA256 签名进行身份验证,方便与 JumpServer PAM API 集成。
## 使用说明
1. **下载 Go 代码文件**
将代码文件下载到您的项目目录中。
2. **导入包**
在您的 Go 文件中导入该包,您即可直接使用其功能。
## 需求
- `Go 1.16+`
- `github.com/google/uuid`
- `gopkg.in/twindagger/httpsig.v1`
## 使用方法
### 初始化
要使用 JumpServer PAM 客户端,通过提供所需的 `endpoint`、`keyID` 和 `keySecret` 创建一个实例。
```go
package main
import (
"fmt"
"your_module_path/jms_pam"
)
func main() {
client := jms_pam.NewJumpServerPAM(
"http://127.0.0.1", // 替换为您的 JumpServer 端点
"your-key-id", // 替换为您的实际 Key ID
"your-key-secret", // 替换为您的实际 Key Secret
"", // 留空以使用默认的组织 ID
)
}
```
### 创建密码请求
您可以通过指定资产或账户信息来创建请求。
```go
request, err := jms_pam.NewSecretRequest("Linux", "", "root", "")
if err != nil {
fmt.Println("创建请求时出错:", err)
return
}
```
### 发送请求
使用客户端的 `Send` 方法发送请求。
```go
secretObj, err := client.Send(request)
if err != nil {
fmt.Println("发送请求时出错:", err)
return
}
```
### 处理响应
检查密码是否成功检索,并相应地处理响应。
```go
if secretObj.Valid {
fmt.Println("密码:", secretObj.Secret)
} else {
fmt.Println("获取密码失败:", string(secretObj.Desc))
}
```
### 完整示例
以下是如何使用该客户端的完整示例:
```go
package main
import (
"fmt"
"your_module_path/jms_pam"
)
func main() {
client := jms_pam.NewJumpServerPAM(
"http://127.0.0.1",
"your-key-id",
"your-key-secret",
"",
)
request, err := jms_pam.NewSecretRequest("Linux", "", "root", "")
if err != nil {
fmt.Println("创建请求时出错:", err)
return
}
secretObj, err := client.Send(request)
if err != nil {
fmt.Println("发送请求时出错:", err)
return
}
if secretObj.Valid {
fmt.Println("密码:", secretObj.Secret)
} else {
fmt.Println("获取密码失败:", string(secretObj.Desc))
}
}
```
## 错误处理
该库会在创建 `SecretRequest` 时返回无效参数的错误。这包括对有效 UUID 的检查以及确保提供了必需的参数。
## 贡献
欢迎贡献!如有任何增强或错误修复,请提出问题或提交拉取请求。

View File

@ -0,0 +1,133 @@
# JumpServer PAM 客戶端
該包提供了一個 Go 客戶端,用於與 JumpServer PAM API 交互,以檢索各種資產的密碼。它簡化了發送請求和處理響應的過程。
## 功能
- 在發送請求之前驗證參數。
- 支持基於資產和帳戶的密碼檢索。
- 使用 HMAC-SHA256 簽名進行身份驗證,方便與 JumpServer PAM API 集成。
## 使用說明
1. **下載 Go 代碼文件**
將代碼文件下載到您的項目目錄中。
2. **導入包**
在您的 Go 文件中導入該包,您即可直接使用其功能。
## 需求
- `Go 1.16+`
- `github.com/google/uuid`
- `gopkg.in/twindagger/httpsig.v1`
## 使用方法
### 初始化
要使用 JumpServer PAM 客戶端,通過提供所需的 `endpoint`、`keyID` 和 `keySecret` 創建一個實例。
```go
package main
import (
"fmt"
"your_module_path/jms_pam"
)
func main() {
client := jms_pam.NewJumpServerPAM(
"http://127.0.0.1", // 替換為您的 JumpServer 端點
"your-key-id", // 替換為您的實際 Key ID
"your-key-secret", // 替換為您的實際 Key Secret
"", // 留空以使用默認的組織 ID
)
}
```
### 創建密碼請求
您可以通過指定資產或帳戶信息來創建請求。
```go
request, err := jms_pam.NewSecretRequest("Linux", "", "root", "")
if err != nil {
fmt.Println("創建請求時出錯:", err)
return
}
```
### 發送請求
使用客戶端的 `Send` 方法發送請求。
```go
secretObj, err := client.Send(request)
if err != nil {
fmt.Println("發送請求時出錯:", err)
return
}
```
### 處理響應
檢查密碼是否成功檢索,並相應地處理響應。
```go
if secretObj.Valid {
fmt.Println("密碼:", secretObj.Secret)
} else {
fmt.Println("獲取密碼失敗:", string(secretObj.Desc))
}
```
### 完整示例
以下是如何使用該客戶端的完整示例:
```go
package main
import (
"fmt"
"your_module_path/jms_pam"
)
func main() {
client := jms_pam.NewJumpServerPAM(
"http://127.0.0.1",
"your-key-id",
"your-key-secret",
"",
)
request, err := jms_pam.NewSecretRequest("Linux", "", "root", "")
if err != nil {
fmt.Println("創建請求時出錯:", err)
return
}
secretObj, err := client.Send(request)
if err != nil {
fmt.Println("發送請求時出錯:", err)
return
}
if secretObj.Valid {
fmt.Println("密碼:", secretObj.Secret)
} else {
fmt.Println("獲取密碼失敗:", string(secretObj.Desc))
}
}
```
## 錯誤處理
該庫會在創建 `SecretRequest` 時返回無效參數的錯誤。這包括對有效 UUID 的檢查以及確保提供了必需的參數。
## 貢獻
歡迎貢獻!如有任何增強或錯誤修復,請提出問題或提交拉取請求。

View File

@ -0,0 +1,162 @@
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"github.com/google/uuid"
"gopkg.in/twindagger/httpsig.v1"
)
const DefaultOrgId = "00000000-0000-0000-0000-000000000002"
type RequestParamsError struct {
Params []string
}
func (e *RequestParamsError) Error() string {
return fmt.Sprintf("At least one of the following fields must be provided: %v.", e.Params)
}
type SecretRequest struct {
AccountID string
AssetID string
Asset string
Account string
Method string
}
func NewSecretRequest(asset, assetID, account, accountID string) (*SecretRequest, error) {
req := &SecretRequest{
Asset: asset,
AssetID: assetID,
Account: account,
AccountID: accountID,
Method: http.MethodGet,
}
return req, req.validate()
}
func (r *SecretRequest) validate() error {
if r.AccountID != "" {
if _, err := uuid.Parse(r.AccountID); err != nil {
return fmt.Errorf("invalid UUID: %s. Value must be a valid UUID", r.AccountID)
}
return nil
}
if r.AssetID == "" && r.Asset == "" {
return &RequestParamsError{Params: []string{"asset", "asset_id"}}
}
if r.Account == "" {
return &RequestParamsError{Params: []string{"account", "account_id"}}
}
if r.AssetID != "" {
if _, err := uuid.Parse(r.AssetID); err != nil {
return fmt.Errorf("invalid UUID: %s. Value must be a valid UUID", r.AssetID)
}
}
return nil
}
func (r *SecretRequest) GetURL() string {
return "/api/v1/accounts/service-integrations/account-secret/"
}
func (r *SecretRequest) GetQuery() url.Values {
query := url.Values{}
if r.AccountID != "" {
query.Add("account_id", r.AccountID)
}
if r.AssetID != "" {
query.Add("asset_id", r.AssetID)
}
if r.Asset != "" {
query.Add("asset", r.Asset)
}
if r.Account != "" {
query.Add("account", r.Account)
}
return query
}
type Secret struct {
Secret string `json:"secret,omitempty"`
Desc json.RawMessage `json:"desc,omitempty"`
Valid bool `json:"valid"`
}
func FromResponse(response *http.Response) Secret {
var secret Secret
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
var raw json.RawMessage
if err := json.NewDecoder(response.Body).Decode(&raw); err == nil {
secret.Desc = raw
} else {
secret.Desc = json.RawMessage(`{"error": "Unknown error occurred"}`)
}
} else {
_ = json.NewDecoder(response.Body).Decode(&secret)
secret.Valid = true
}
return secret
}
type JumpServerPAM struct {
Endpoint string
KeyID string
KeySecret string
OrgID string
httpClient *http.Client
}
func NewJumpServerPAM(endpoint, keyID, keySecret, orgID string) *JumpServerPAM {
if orgID == "" {
orgID = DefaultOrgId
}
return &JumpServerPAM{
Endpoint: endpoint,
KeyID: keyID,
KeySecret: keySecret,
OrgID: orgID,
httpClient: &http.Client{},
}
}
func (c *JumpServerPAM) SignRequest(r *http.Request) error {
headers := []string{"(request-target)", "date"}
signer, err := httpsig.NewRequestSigner(c.KeyID, c.KeySecret, "hmac-sha256")
if err != nil {
return err
}
return signer.SignRequest(r, headers, nil)
}
func (c *JumpServerPAM) Send(req *SecretRequest) (Secret, error) {
fullUrl := c.Endpoint + req.GetURL()
query := req.GetQuery()
fullURL := fmt.Sprintf("%s?%s", fullUrl, query.Encode())
request, err := http.NewRequest(req.Method, fullURL, nil)
if err != nil {
return Secret{}, err
}
request.Header.Set("Accept", "application/json")
request.Header.Set("X-Source", "jms-pam")
err = c.SignRequest(request)
if err != nil {
return Secret{Desc: json.RawMessage(`{"error": "` + err.Error() + `"}`)}, nil
}
response, err := c.httpClient.Do(request)
if err != nil {
return Secret{Desc: json.RawMessage(`{"error": "` + err.Error() + `"}`)}, nil
}
return FromResponse(response), nil
}

View File

@ -0,0 +1,96 @@
# JumpServer PAM Client
This package provides a Python client for interacting with the JumpServer PAM API to retrieve secrets for various assets. It simplifies the process of sending requests and handling responses.
## Features
- Validate parameters before sending requests.
- Support for both asset and account-based secret retrieval.
- Easy integration with JumpServer PAM API using HTTP signatures for authentication.
## Installation
You can install the package via pip:
```bash
pip install jms_pam-0.0.1-py3-none-any.whl
```
## Requirements
- `Python 3.6+`
- `requests`
- `httpsig`
## Usage
### Initialization
To use the JumpServer PAM client, create an instance by providing the required `endpoint`, `key_id`, and `key_secret`.
```python
from jms_pam import JumpServerPAM, SecretRequest
client = JumpServerPAM(
endpoint='http://127.0.0.1',
key_id='your-key-id',
key_secret='your-key-secret'
)
```
### Creating a Secret Request
You can create a request for a secret by specifying the asset or account information.
```python
request = SecretRequest(asset='Linux', account='root')
```
### Sending the Request
Send the request using the `send` method of the client.
```python
secret_obj = client.send(request)
```
### Handling the Response
Check if the secret was retrieved successfully and handle the response accordingly.
```python
if secret_obj.valid:
print('Secret: %s' % secret_obj.secret)
else:
print('Get secret failed: %s' % secret_obj.desc)
```
### Complete Example
Heres a complete example of how to use the client:
```python
from jumpserver_pam_client import JumpServerPAM, SecretRequest
client = JumpServerPAM(
endpoint='http://127.0.0.1',
key_id='your-key-id',
key_secret='your-key-secret'
)
request = SecretRequest(asset='Linux', account='root')
secret_obj = client.send(request)
if secret_obj.valid:
print('Secret: %s' % secret_obj.secret)
else:
print('Get secret failed: %s' % secret_obj.desc)
```
## Error Handling
The library raises `RequestParamsError` if the parameters provided do not meet the validation requirements. This includes checks for valid UUIDs and interdependencies between parameters.
## Contributing
Contributions are welcome! Please open an issue or submit a pull request for any enhancements or bug fixes.

View File

@ -0,0 +1,96 @@
# JumpServer PAM クライアント
このパッケージは、JumpServer PAM API と対話し、さまざまなアセットのシークレットを取得するための Python クライアントを提供します。リクエストを送信し、レスポンスを処理するプロセスを簡素化します。
## 特徴
- リクエストを送信する前にパラメータを検証します。
- アセットおよびアカウントベースのシークレット取得をサポートします。
- HTTP 署名を使用して JumpServer PAM API と簡単に統合できます。
## インストール
以下のコマンドを使用して、パッケージを pip でインストールできます:
```bash
pip install jms_pam-0.0.1-py3-none-any.whl
```
## 要件
- `Python 3.6+`
- `requests`
- `httpsig`
## 使用方法
### 初期化
JumpServer PAM クライアントを使用するには、必要な `endpoint`、`key_id`、および `key_secret` を提供してインスタンスを作成します。
```python
from jms_pam import JumpServerPAM, SecretRequest
client = JumpServerPAM(
endpoint='http://127.0.0.1',
key_id='your-key-id',
key_secret='your-key-secret'
)
```
### シークレットリクエストの作成
アセットまたはアカウント情報を指定して、シークレットのリクエストを作成できます。
```python
request = SecretRequest(asset='Linux', account='root')
```
### リクエストの送信
クライアントの `send` メソッドを使用してリクエストを送信します。
```python
secret_obj = client.send(request)
```
### レスポンスの処理
シークレットが正常に取得されたかどうかを確認し、レスポンスを適切に処理します。
```python
if secret_obj.valid:
print('秘密: %s' % secret_obj.secret)
else:
print('シークレットの取得に失敗しました: %s' % secret_obj.desc)
```
### 完全な例
以下は、クライアントの使用方法の完全な例です:
```python
from jumpserver_pam_client import JumpServerPAM, SecretRequest
client = JumpServerPAM(
endpoint='http://127.0.0.1',
key_id='your-key-id',
key_secret='your-key-secret'
)
request = SecretRequest(asset='Linux', account='root')
secret_obj = client.send(request)
if secret_obj.valid:
print('秘密: %s' % secret_obj.secret)
else:
print('シークレットの取得に失敗しました: %s' % secret_obj.desc)
```
## エラーハンドリング
ライブラリは、提供されたパラメータが検証要件を満たしていない場合に `RequestParamsError` を発生させます。これには、有効な UUID の確認やパラメータ間の相互依存性のチェックが含まれます。
## 貢献
貢献を歓迎します!改善やバグ修正のために、問題を開くかプルリクエストを送信してください。

View File

@ -0,0 +1,96 @@
# JumpServer PAM 客户端
该包提供了一个 Python 客户端,用于与 JumpServer PAM API 交互,以检索各种资产的密码。它简化了发送请求和处理响应的过程。
## 特性
- 在发送请求之前验证参数。
- 支持基于资产和账户的密码检索。
- 通过 HTTP 签名轻松集成 JumpServer PAM API。
## 安装
您可以通过 pip 安装该包:
```bash
pip install jms_pam-0.0.1-py3-none-any.whl
```
## 需求
- `Python 3.6+`
- `requests`
- `httpsig`
## 使用方法
### 初始化
要使用 JumpServer PAM 客户端,通过提供所需的 `endpoint`、`key_id` 和 `key_secret` 创建一个实例。
```python
from jms_pam import JumpServerPAM, SecretRequest
client = JumpServerPAM(
endpoint='http://127.0.0.1',
key_id='your-key-id',
key_secret='your-key-secret'
)
```
### 创建密码请求
您可以通过指定资产或账户信息来创建一个密码请求。
```python
request = SecretRequest(asset='Linux', account='root')
```
### 发送请求
使用客户端的 `send` 方法发送请求。
```python
secret_obj = client.send(request)
```
### 处理响应
检查密码是否成功检索,并相应地处理响应。
```python
if secret_obj.valid:
print('密码: %s' % secret_obj.secret)
else:
print('获取密码失败: %s' % secret_obj.desc)
```
### 完整示例
以下是如何使用该客户端的完整示例:
```python
from jms_pam import JumpServerPAM, SecretRequest
client = JumpServerPAM(
endpoint='http://127.0.0.1',
key_id='your-key-id',
key_secret='your-key-secret'
)
request = SecretRequest(asset='Linux', account='root')
secret_obj = client.send(request)
if secret_obj.valid:
print('密码: %s' % secret_obj.secret)
else:
print('获取密码失败: %s' % secret_obj.desc)
```
## 错误处理
如果提供的参数不符合验证要求,库会引发 `RequestParamsError`。这包括对有效 UUID 的检查和参数之间的相互依赖性检查。
## 贡献
欢迎贡献!请打开一个问题或提交拉取请求,以进行任何增强或修复错误。

View File

@ -0,0 +1,96 @@
# JumpServer PAM 客戶端
此套件提供了一個 Python 客戶端,用於與 JumpServer PAM API 互動,以檢索各種資產的秘密。它簡化了發送請求和處理響應的過程。
## 特性
- 在發送請求之前驗證參數。
- 支持基於資產和帳戶的秘密檢索。
- 通過 HTTP 簽名輕鬆集成 JumpServer PAM API。
## 安裝
您可以通過 pip 安裝此套件:
```bash
pip install jms_pam-0.0.1-py3-none-any.whl
```
## 需求
- `Python 3.6+`
- `requests`
- `httpsig`
## 使用方法
### 初始化
要使用 JumpServer PAM 客戶端,通過提供所需的 `endpoint`、`key_id` 和 `key_secret` 創建一個實例。
```python
from jms_pam import JumpServerPAM, SecretRequest
client = JumpServerPAM(
endpoint='http://127.0.0.1',
key_id='your-key-id',
key_secret='your-key-secret'
)
```
### 創建秘密請求
您可以通過指定資產或帳戶信息來創建一個秘密請求。
```python
request = SecretRequest(asset='Linux', account='root')
```
### 發送請求
使用客戶端的 `send` 方法發送請求。
```python
secret_obj = client.send(request)
```
### 處理響應
檢查秘密是否成功檢索,並相應地處理響應。
```python
if secret_obj.valid:
print('秘密: %s' % secret_obj.secret)
else:
print('獲取秘密失敗: %s' % secret_obj.desc)
```
### 完整示例
以下是如何使用該客戶端的完整示例:
```python
from jumpserver_pam_client import JumpServerPAM, SecretRequest
client = JumpServerPAM(
endpoint='http://127.0.0.1',
key_id='your-key-id',
key_secret='your-key-secret'
)
request = SecretRequest(asset='Linux', account='root')
secret_obj = client.send(request)
if secret_obj.valid:
print('秘密: %s' % secret_obj.secret)
else:
print('獲取秘密失敗: %s' % secret_obj.desc)
```
## 錯誤處理
如果提供的參數不符合驗證要求,該庫將引發 `RequestParamsError`。這包括對有效 UUID 的檢查和參數之間的相互依賴性檢查。
## 貢獻
歡迎貢獻!請打開一個問題或提交拉取請求,以進行任何增強或修復錯誤。

View File

@ -0,0 +1 @@
from .main import JumpServerPAM, SecretRequest

View File

@ -0,0 +1,145 @@
import requests
import uuid
from datetime import datetime
from urllib.parse import urlencode
from httpsig.requests_auth import HTTPSignatureAuth
from requests.exceptions import RequestException
DEFAULT_ORG_ID = '00000000-0000-0000-0000-000000000002'
class RequestParamsError(ValueError):
def __init__(self, params):
self.params = params
def __str__(self):
msg = "At least one of the following fields must be provided: %s."
return 'RequestParamsError: (%s)' % msg % ', '.join(self.params)
class SecretRequest(object):
"""
Validate parameters and their interdependencies.
Parameters:
account_id (str): The account ID, must be a valid UUID.
asset_id (str): The asset ID, must be a valid UUID.
asset (str): The name of the asset, can be empty.
account (str): The name of the account, can be empty.
Validation Logic:
- When 'account_id' is provided, 'asset', 'asset_id', and 'account' must not be provided.
- When 'account' is provided, either 'asset' or 'asset_id' must be provided.
- It is not allowed to provide both 'account_id' and 'asset_id' together.
Raises:
ValueError: If the parameters do not meet the requirements, a detailed error message will be raised.
"""
def __init__(self, asset='', asset_id='', account='', account_id=''):
self.account_id = account_id
self.asset_id = asset_id
self.asset = asset
self.account = account
self.method = 'get'
self._init_check()
@staticmethod
def _valid_uuid(value):
if not value:
return
try:
uuid.UUID(str(value))
except (ValueError, TypeError):
raise ValueError('Invalid UUID: %s. Value must be a valid UUID.' % value)
def _init_check(self):
for id_value in [self.account_id, self.asset_id]:
self._valid_uuid(id_value)
if self.account_id:
return
if not self.asset_id and not self.asset:
raise RequestParamsError(['asset', 'asset_id'])
if not self.account:
raise RequestParamsError(['account', 'account_id'])
@staticmethod
def get_url():
return '/api/v1/accounts/service-integrations/account-secret/'
def get_query(self):
return {k: getattr(self, k) for k in vars(self) if getattr(self, k)}
class Secret(object):
def __init__(self, secret='', desc=''):
self.secret = secret
self.desc = desc
self.valid = not desc
@classmethod
def from_exception(cls, e):
return cls(desc=str(e))
@classmethod
def from_response(cls, response):
secret, error = '', ''
try:
data = response.json()
if response.status_code != 200:
for k, v in data.items():
error += '%s: %s; ' % (k, v)
secret = data.get('secret')
except Exception as e:
error = str(e)
return cls(secret=secret, desc=error)
class JumpServerPAM(object):
def __init__(self, endpoint, key_id, key_secret, org_id=DEFAULT_ORG_ID):
self.endpoint = endpoint
self.key_id = key_id
self.key_secret = key_secret
self.org_id = org_id
self._auth = None
@property
def headers(self):
gmt_form = '%a, %d %b %Y %H:%M:%S GMT'
return {
'Accept': 'application/json',
'X-JMS-ORG': self.org_id,
'Date': datetime.utcnow().strftime(gmt_form),
'X-Source': 'jms-pam'
}
def _build_url(self, url, query_params=None):
query_params = query_params or {}
endpoint = self.endpoint[:-1] if self.endpoint.endswith('/') else self.endpoint
return '%s%s?%s' % (endpoint, url, urlencode(query_params))
def _get_auth(self):
if self._auth is None:
signature_headers = ['(request-target)', 'accept', 'date']
self._auth = HTTPSignatureAuth(
key_id=self.key_id, secret=self.key_secret,
algorithm='hmac-sha256', headers=signature_headers
)
return self._auth
def send(self, secret_request):
try:
url = secret_request.get_url()
query_params = secret_request.get_query()
request_method = getattr(requests, secret_request.method)
response = request_method(
self._build_url(url, query_params),
auth=self._get_auth(), headers=self.headers
)
except RequestException as e:
return Secret.from_exception(e)
return Secret.from_response(response)

View File

@ -0,0 +1,22 @@
from setuptools import setup, find_packages
setup(
name='jms-pam',
version='0.0.1',
packages=find_packages(),
install_requires=[
'requests',
'httpsig'
],
description='JumpServer PAM Client',
long_description=open('README.md').read(),
long_description_content_type='text/markdown',
url='https://github.com/jumpserver',
author='JumpServer Team',
author_email='code@jumpserver.org',
classifiers=[
'Programming Language :: Python :: 3',
],
python_requires='>=3.6',
)

View File

@ -0,0 +1,41 @@
# Generated by Django 4.1.13 on 2024-11-29 14:41
import common.db.fields
import common.db.utils
from django.db import migrations, models
import private_storage.fields
import private_storage.storage.files
import uuid
class Migration(migrations.Migration):
dependencies = [
('accounts', '0014_gatheraccountsautomation_check_risk'),
]
operations = [
migrations.CreateModel(
name='ServiceIntegration',
fields=[
('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')),
('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('name', models.CharField(max_length=128, verbose_name='Name')),
('logo_image', private_storage.fields.PrivateImageField(max_length=128, storage=private_storage.storage.files.PrivateFileSystemStorage(), upload_to='service-integration', verbose_name='Logo')),
('secret', common.db.fields.EncryptTextField(default='', verbose_name='Secret')),
('accounts', common.db.fields.JSONManyToManyField(default=dict, to='accounts.Account', verbose_name='Accounts')),
('ip_group', models.JSONField(default=common.db.utils.default_ip_group, verbose_name='IP group')),
('date_last_used', models.DateTimeField(blank=True, null=True, verbose_name='Date last used')),
('is_active', models.BooleanField(default=True, verbose_name='Active')),
],
options={
'verbose_name': 'Service integration',
'unique_together': {('name', 'org_id')},
},
),
]

View File

@ -3,3 +3,4 @@ from .base import * # noqa
from .automations import * # noqa
from .template import * # noqa
from .virtual import * # noqa
from .service import * # noqa

View File

@ -1,4 +1,5 @@
from django.db import models
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from accounts.const import (
@ -48,3 +49,9 @@ class ChangeSecretRecord(JMSBaseModel):
def __str__(self):
return f'{self.account.username}@{self.asset}'
@staticmethod
def get_valid_records():
return ChangeSecretRecord.objects.exclude(
Q(execution__isnull=True) | Q(asset__isnull=True) | Q(account__isnull=True)
)

View File

@ -0,0 +1,65 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from private_storage.fields import PrivateImageField
from accounts.models import Account
from common.db import fields
from common.db.fields import JSONManyToManyField, RelatedManager
from common.db.utils import default_ip_group
from common.utils import random_string
from orgs.mixins.models import JMSOrgBaseModel
class ServiceIntegration(JMSOrgBaseModel):
is_anonymous = False
name = models.CharField(max_length=128, unique=False, verbose_name=_('Name'))
logo_image = PrivateImageField(
upload_to='service-integration', max_length=128, verbose_name=_('Logo')
)
secret = fields.EncryptTextField(default='', verbose_name=_('Secret'))
accounts = JSONManyToManyField('accounts.Account', default=dict, verbose_name=_('Accounts'))
ip_group = models.JSONField(default=default_ip_group, verbose_name=_('IP group'))
date_last_used = models.DateTimeField(null=True, blank=True, verbose_name=_('Date last used'))
is_active = models.BooleanField(default=True, verbose_name=_('Active'))
class Meta:
unique_together = [('name', 'org_id')]
verbose_name = _('Service integration')
@property
def accounts_amount(self):
qs = Account.objects.all()
query = RelatedManager.get_to_filter_qs(self.accounts.value, Account)
return qs.filter(*query).count()
@property
def is_valid(self):
return self.is_active
@property
def is_authenticated(self):
return self.is_active
@staticmethod
def has_perms(perms):
support_perms = ['accounts.view_serviceintegration']
return all([perm in support_perms for perm in perms])
def get_secret(self):
self.secret = random_string(36)
self.save(update_fields=['secret'])
return self.secret
def get_account(self, asset='', asset_id='', account='', account_id=''):
qs = Account.objects.all()
if account_id:
qs = qs.filter(id=account_id)
elif account:
qs = qs.filter(name=account)
if asset_id:
qs = qs.filter(asset_id=asset_id)
elif asset:
qs = qs.filter(asset__name=asset)
query = RelatedManager.get_to_filter_qs(self.accounts.value, Account)
return qs.filter(*query).distinct().first()

View File

@ -3,3 +3,4 @@ from .backup import *
from .base import *
from .template import *
from .virtual import *
from .service import *

View File

@ -0,0 +1,56 @@
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from accounts.models import ServiceIntegration
from acls.serializers.rules import ip_group_child_validator, ip_group_help_text
from common.serializers.fields import JSONManyToManyField
class ServiceIntegrationSerializer(serializers.ModelSerializer):
accounts = JSONManyToManyField(label=_('Account'))
ip_group = serializers.ListField(
default=['*'], label=_('Access IP'), help_text=ip_group_help_text,
child=serializers.CharField(max_length=1024, validators=[ip_group_child_validator])
)
class Meta:
model = ServiceIntegration
fields_mini = ['id', 'name']
fields_small = fields_mini + ['logo_image', 'accounts']
fields = fields_small + [
'date_last_used', 'date_created', 'date_updated',
'ip_group', 'accounts_amount', 'comment', 'is_active'
]
extra_kwargs = {
'comment': {'label': _('Comment')},
'name': {'label': _('Name')},
'is_active': {'default': True},
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
request_method = self.context.get('request').method
if request_method == 'PUT':
self.fields['logo_image'].required = False
class ServiceAccountSecretSerializer(serializers.Serializer):
asset = serializers.CharField(required=False, allow_blank=True)
asset_id = serializers.UUIDField(required=False, allow_null=True)
account = serializers.CharField(required=False, allow_blank=True)
account_id = serializers.UUIDField(required=False, allow_null=True)
@staticmethod
def _valid_at_least_one(attrs, fields):
if not any(attrs.get(field) for field in fields):
raise serializers.ValidationError(
f"At least one of the following fields must be provided: {', '.join(fields)}."
)
def validate(self, attrs):
if attrs.get('account_id'):
return attrs
self._valid_at_least_one(attrs, ['asset', 'asset_id'])
self._valid_at_least_one(attrs, ['account', 'account_id'])
return attrs

View File

@ -9,6 +9,7 @@ app_name = 'accounts'
router = BulkRouter()
router.register(r'accounts', api.AccountViewSet, 'account')
router.register(r'service-integrations', api.ServiceIntegrationViewSet, 'service-integration')
router.register(r'virtual-accounts', api.VirtualAccountViewSet, 'virtual-account')
router.register(r'gathered-accounts', api.GatheredAccountViewSet, 'gathered-account')
router.register(r'account-secrets', api.AccountSecretsViewSet, 'account-secret')
@ -48,6 +49,7 @@ urlpatterns = [
path('push-account/<uuid:pk>/nodes/', api.PushAccountNodeAddRemoveApi.as_view(),
name='push-account-add-or-remove-node'),
path('push-account/<uuid:pk>/assets/', api.PushAccountAssetsListApi.as_view(), name='push-account-assets'),
path('change-secret-dashboard/', api.ChangeSecretDashboardApi.as_view(), name='change-secret-dashboard'),
]
urlpatterns += router.urls

View File

@ -33,13 +33,13 @@ from .const import ActivityChoices
from .filters import UserSessionFilterSet, OperateLogFilterSet
from .models import (
FTPLog, UserLoginLog, OperateLog, PasswordChangeLog,
ActivityLog, JobLog, UserSession
ActivityLog, JobLog, UserSession, ServiceAccessLog
)
from .serializers import (
FTPLogSerializer, UserLoginLogSerializer, JobLogSerializer,
OperateLogSerializer, OperateLogActionDetailSerializer,
PasswordChangeLogSerializer, ActivityUnionLogSerializer,
FileSerializer, UserSessionSerializer
FileSerializer, UserSessionSerializer, ServiceAccessLogSerializer
)
from .utils import construct_userlogin_usernames
@ -290,3 +290,15 @@ class UserSessionViewSet(CommonApiMixin, viewsets.ModelViewSet):
user_session_manager.remove(key)
queryset.delete()
return Response(status=status.HTTP_200_OK)
class ServiceAccessLogViewSet(OrgReadonlyModelViewSet):
model = ServiceAccessLog
serializer_class = ServiceAccessLogSerializer
extra_filter_backends = [DatetimeRangeFilterBackend]
date_range_filter_fields = [
('datetime', ('date_from', 'date_to'))
]
filterset_fields = ['account', 'remote_addr', 'service_id']
search_fields = filterset_fields
ordering = ['-datetime']

View File

@ -63,7 +63,7 @@ class OperateLogFilterSet(BaseFilterSet):
with translation.override(current_lang):
mapper = {str(m._meta.verbose_name): m._meta.verbose_name_raw for m in apps.get_models()}
tp = mapper.get(resource_type)
queryset = queryset.filter(resource_type=tp)
queryset = queryset.filter(resource_type__in=[tp, resource_type])
return queryset
class Meta:

View File

@ -0,0 +1,26 @@
# Generated by Django 4.1.13 on 2024-11-28 09:48
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
('audits', '0003_auto_20180816_1652'),
]
operations = [
migrations.CreateModel(
name='ServiceAccessLog',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('remote_addr', models.GenericIPAddressField(verbose_name='Remote addr')),
('service', models.CharField(max_length=128, verbose_name='Service')),
('service_id', models.UUIDField(verbose_name='Service ID')),
('asset', models.CharField(max_length=128, verbose_name='Asset')),
('account', models.CharField(max_length=128, verbose_name='Account')),
('datetime', models.DateTimeField(auto_now=True, verbose_name='Datetime')),
],
),
]

View File

@ -33,7 +33,8 @@ __all__ = [
"PasswordChangeLog",
"UserLoginLog",
"JobLog",
"UserSession"
"UserSession",
"ServiceAccessLog",
]
@ -301,3 +302,13 @@ class UserSession(models.Model):
permissions = [
('offline_usersession', _('Offline user session')),
]
class ServiceAccessLog(models.Model):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
remote_addr = models.GenericIPAddressField(verbose_name=_("Remote addr"))
service = models.CharField(max_length=128, verbose_name=_("Service"))
service_id = models.UUIDField(verbose_name=_("Service ID"))
asset = models.CharField(max_length=128, verbose_name=_("Asset"))
account = models.CharField(max_length=128, verbose_name=_("Account"))
datetime = models.DateTimeField(auto_now=True, verbose_name=_("Datetime"))

View File

@ -189,3 +189,19 @@ class UserSessionSerializer(serializers.ModelSerializer):
if not request:
return False
return request.session.session_key == obj.key
class ServiceAccessLogSerializer(serializers.ModelSerializer):
class Meta:
model = models.ServiceAccessLog
fields_mini = ['id']
fields_small = fields_mini + [
'remote_addr', 'service', 'service_id', 'asset', 'account', 'datetime'
]
fields = fields_small
extra_kwargs = {
'remote_addr': {'label': _('Remote Address')},
'asset': {'label': _('Asset')},
'account': {'label': _('Account')},
'datetime': {'label': _('Datetime')},
}

View File

@ -16,6 +16,7 @@ router.register(r'password-change-logs', api.PasswordChangeLogViewSet, 'password
router.register(r'job-logs', api.JobAuditViewSet, 'job-log')
router.register(r'my-login-logs', api.MyLoginLogViewSet, 'my-login-log')
router.register(r'user-sessions', api.UserSessionViewSet, 'user-session')
router.register(r'service-access-logs', api.ServiceAccessLogViewSet, 'service-access-log')
urlpatterns = [
path('activities/', api.ResourceActivityAPIView.as_view(), name='resource-activities'),

View File

@ -7,9 +7,10 @@ from django.utils import timezone
from django.utils.translation import gettext as _
from rest_framework import authentication, exceptions
from accounts.models import ServiceIntegration
from common.auth import signature
from common.decorators import merge_delay_run
from common.utils import get_object_or_none, get_request_ip_or_data, contains_ip
from common.utils import get_object_or_none, get_request_ip_or_data, contains_ip, get_request_ip
from users.models import User
from ..models import AccessKey, PrivateToken
@ -33,6 +34,13 @@ def update_user_last_used(users=()):
User.objects.filter(id__in=users).update(date_api_key_last_used=timezone.now())
@merge_delay_run(ttl=60)
def update_service_integration_last_used(service_integrations=()):
ServiceIntegration.objects.filter(
id__in=service_integrations
).update(date_last_used=timezone.now())
def after_authenticate_update_date(user, token=None):
update_user_last_used.delay(users=(user.id,))
if token:
@ -146,3 +154,30 @@ class SignatureAuthentication(signature.SignatureAuthentication):
return True
except (AccessKey.DoesNotExist, exceptions.ValidationError):
return False
class ServiceAuthentication(signature.SignatureAuthentication):
__instance = None
source = 'jms-pam'
def get_object(self, key_id):
if not self.__instance:
self.__instance = ServiceIntegration.objects.filter(
id=key_id, is_active=True,
).first()
return self.__instance
def fetch_user_data(self, key_id, algorithm=None):
obj = self.get_object(key_id)
if not obj:
return None, None
return obj, obj.secret
def is_ip_allow(self, key_id, request):
obj = self.get_object(key_id)
if not contains_ip(get_request_ip(request), obj.ip_group):
return False
return True
def after_authenticate_update_date(self, user):
update_service_integration_last_used.delay((user.id,))

View File

@ -5,6 +5,8 @@ from django.db import models
from django.utils.translation import gettext_lazy as _
import common.db.models
from common.db.utils import default_ip_group
from common.utils.random import random_string
@ -12,10 +14,6 @@ def default_secret():
return random_string(36)
def default_ip_group():
return ["*"]
class AccessKey(models.Model):
id = models.UUIDField(verbose_name='AccessKeyID', primary_key=True, default=uuid.uuid4, editable=False)
secret = models.CharField(verbose_name='AccessKeySecret', default=default_secret, max_length=36)

View File

@ -36,7 +36,7 @@ class SignatureAuthentication(authentication.BaseAuthentication):
:param www_authenticate_realm: Default: "api"
:param required_headers: Default: ["(request-target)", "date"]
"""
source = ''
www_authenticate_realm = "api"
required_headers = ["(request-target)", "date"]
@ -47,6 +47,9 @@ class SignatureAuthentication(authentication.BaseAuthentication):
def is_ip_allow(self, key_id, request):
raise NotImplementedError()
def after_authenticate_update_date(self, user):
pass
def authenticate_header(self, request):
"""
DRF sends this for unauthenticated responses if we're the primary
@ -74,6 +77,9 @@ class SignatureAuthentication(authentication.BaseAuthentication):
if method.lower() != 'signature':
return None
if self.source and request.META.get('HTTP_X_SOURCE') != self.source:
return None
# Verify basic header structure.
if len(fields) == 0:
raise FAILED
@ -117,4 +123,5 @@ class SignatureAuthentication(authentication.BaseAuthentication):
if not hs.verify():
raise FAILED
self.after_authenticate_update_date(user)
return user, fields["keyid"]

View File

@ -39,6 +39,7 @@ __all__ = [
"BitChoices",
"TreeChoices",
"JSONManyToManyField",
"RelatedManager",
]

View File

@ -8,6 +8,10 @@ from common.utils import get_logger, signer, crypto
logger = get_logger(__file__)
def default_ip_group():
return ["*"]
def get_object_if_need(model, pk):
if not isinstance(pk, model):
try:

View File

@ -25,7 +25,7 @@ __all__ = [
'IDInFilterBackend', "CustomFilterBackend",
"BaseFilterSet", 'IDNotFilterBackend',
'NotOrRelFilterBackend', 'LabelFilterBackend',
'RewriteOrderingFilter'
'RewriteOrderingFilter', 'AttrRulesFilterBackend'
]

View File

@ -297,6 +297,7 @@ class JSONManyToManyField(serializers.JSONField):
if not data:
data = {}
try:
data = super().to_internal_value(data)
ModelJSONManyToManyField.check_value(data)
except ValueError as e:
raise serializers.ValidationError(e)

View File

@ -8,6 +8,7 @@
"AccessIP": "IP whitelist",
"AccessKey": "Access key",
"Account": "Account",
"AccountAmount": "Account amount",
"AccountBackup": "Backup accounts",
"AccountBackupCreate": "Create account backup",
"AccountBackupDetail": "Backup account details",
@ -241,6 +242,7 @@
"CAS": "CAS",
"CMPP2": "Cmpp v2.0",
"CalculationResults": "Error in cron expression",
"CallRecords": "Call Records",
"CanDragSelect": "Select time period by dragging mouse;No selection means all selected",
"Cancel": "Cancel",
"CancelCollection": "Cancel favorite",
@ -403,6 +405,7 @@
"DateLastLogin": "Last login date",
"DateLastMonth": "Last month",
"DateLastSync": "Last sync",
"DataLastUsed": "Last used",
"DateLastWeek": "Last week",
"DateLastYear": "Last year",
"DatePasswordLastUpdated": "Last password update date",
@ -1083,6 +1086,7 @@
"ServerAccountKey": "Service account key",
"ServerError": "Server error",
"ServerTime": "Server time",
"ServiceIntegration": "Service integration",
"Session": "Session",
"SessionCommands": "Session commands",
"SessionConnectTrend": "Session connection trends",

View File

@ -8,6 +8,7 @@
"AccessIP": "IP ホワイトリスト",
"AccessKey": "アクセスキー",
"Account": "アカウント情報",
"AccountAmount": "アカウント数",
"AccountBackup": "アカウントのバックアップ",
"AccountBackupCreate": "アカウントバックアップを作成",
"AccountBackupDetail": "アカウントバックアップの詳細",
@ -254,6 +255,7 @@
"CMPP2": "CMPP v2.0",
"CTYunPrivate": "天翼プライベートクラウド",
"CalculationResults": "cron 式のエラー",
"CallRecords": "つうわきろく",
"CanDragSelect": "マウスドラッグで時間帯を選択可能;未選択は全選択と同じです",
"Cancel": "キャンセル",
"CancelCollection": "お気に入りキャンセル",
@ -418,6 +420,7 @@
"DateLastLogin": "最後にログインした日",
"DateLastMonth": "最近一ヶ月",
"DateLastSync": "最終同期日",
"DataLastUsed": "さいごしようび",
"DateLastWeek": "最新の一週間",
"DateLastYear": "最近一年",
"DatePasswordLastUpdated": "最終パスワード更新日",
@ -1118,6 +1121,7 @@
"ServerAccountKey": "サービスアカウントキー",
"ServerError": "サーバーエラー",
"ServerTime": "サーバータイム",
"ServiceIntegration": "サービス統合",
"Session": "コンバセーション",
"SessionCommands": "セッションアクション",
"SessionConnectTrend": "セッションの接続トレンド",

View File

@ -7,6 +7,7 @@
"Accept": "同意",
"AccessIP": "IP 白名单",
"AccessKey": "访问密钥",
"AccountAmount": "账号数量",
"AccountBackup": "账号备份",
"AccountBackupCreate": "创建账号备份",
"AccountBackupDetail": "账号备份详情",
@ -241,6 +242,7 @@
"CAS": "CAS",
"CMPP2": "CMPP v2.0",
"CalculationResults": "cron 表达式错误",
"CallRecords": "调用记录",
"CanDragSelect": "可拖动鼠标选择时间段;未选择等同全选",
"Cancel": "取消",
"CancelCollection": "取消收藏",
@ -403,6 +405,7 @@
"DateLastLogin": "最后登录日期",
"DateLastMonth": "最近一月",
"DateLastSync": "最后同步日期",
"DataLastUsed": "最后使用日期",
"DateLastWeek": "最近一周",
"DateLastYear": "最近一年",
"DatePasswordLastUpdated": "最后更新密码日期",
@ -1086,6 +1089,7 @@
"ServerAccountKey": "服务账号密钥",
"ServerError": "服务器错误",
"ServerTime": "服务器时间",
"ServiceIntegration": "服务对接",
"Session": "会话",
"SessionCommands": "会话命令",
"SessionConnectTrend": "会话连接趋势",

View File

@ -8,6 +8,7 @@
"AccessIP": "IP 白名單",
"AccessKey": "訪問金鑰",
"Account": "雲帳號",
"AccountAmount": "帳號數量",
"AccountBackup": "帳號備份",
"AccountBackupCreate": "創建帳號備份",
"AccountBackupDetail": "賬號備份詳情",
@ -324,7 +325,8 @@
"CASSetting": "CAS 配置",
"CMPP2": "CMPP v2.0",
"CTYunPrivate": "天翼私有雲",
"CalculationResults": "Cron expression error",
"CalculationResults": "呼叫記錄",
"CallRecords": "調用記錄",
"CanDragSelect": "可拖動滑鼠選擇時間段;未選擇等同全選",
"Cancel": "取消",
"CancelCollection": "取消收藏",
@ -534,6 +536,7 @@
"DateLastMonth": "最近一月",
"DateLastRun": "上次運行日期",
"DateLastSync": "最後同步日期",
"DataLastUsed": "最後使用日期",
"DateLastWeek": "最近一週",
"DateLastYear": "最近一年",
"DatePasswordLastUpdated": "最後更新密碼日期",
@ -1436,6 +1439,7 @@
"ServerAccountKey": "服務帳號金鑰",
"ServerError": "伺服器錯誤",
"ServerTime": "伺服器時間",
"ServiceIntegration": "服務對接",
"ServiceRatio": "組件負載統計",
"Session": "會話",
"SessionCommands": "會話指令",

View File

@ -32,6 +32,7 @@ REST_FRAMEWORK = {
# 'rest_framework.authentication.BasicAuthentication',
'authentication.backends.drf.AccessTokenAuthentication',
'authentication.backends.drf.PrivateTokenAuthentication',
'authentication.backends.drf.ServiceAuthentication',
'authentication.backends.drf.SignatureAuthentication',
'authentication.backends.drf.SessionAuthentication',
),

View File

@ -29,7 +29,8 @@ __all__ = [
"User",
"UserPasswordHistory",
"MFAMixin",
"AuthMixin"
"AuthMixin",
"RoleMixin"
]