mirror of https://github.com/jumpserver/jumpserver
Merge branch 'pam' of github.com:jumpserver/jumpserver into pam
commit
c8a632ed60
|
@ -2,3 +2,4 @@ from .account import *
|
|||
from .task import *
|
||||
from .template import *
|
||||
from .virtual import *
|
||||
from .service import *
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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})
|
||||
|
|
@ -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 *
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
Here’s 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.
|
|
@ -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 の確認や、必要なパラメータが提供されていることの確認が含まれます。
|
||||
|
||||
## 貢献
|
||||
|
||||
貢献を歓迎します!改善やバグ修正のために問題を提起したり、プルリクエストを送信してください。
|
|
@ -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 的检查以及确保提供了必需的参数。
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎贡献!如有任何增强或错误修复,请提出问题或提交拉取请求。
|
|
@ -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 的檢查以及確保提供了必需的參數。
|
||||
|
||||
## 貢獻
|
||||
|
||||
歡迎貢獻!如有任何增強或錯誤修復,請提出問題或提交拉取請求。
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
||||
Here’s 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.
|
|
@ -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 の確認やパラメータ間の相互依存性のチェックが含まれます。
|
||||
|
||||
## 貢献
|
||||
|
||||
貢献を歓迎します!改善やバグ修正のために、問題を開くかプルリクエストを送信してください。
|
|
@ -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 的检查和参数之间的相互依赖性检查。
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎贡献!请打开一个问题或提交拉取请求,以进行任何增强或修复错误。
|
|
@ -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 的檢查和參數之間的相互依賴性檢查。
|
||||
|
||||
## 貢獻
|
||||
|
||||
歡迎貢獻!請打開一個問題或提交拉取請求,以進行任何增強或修復錯誤。
|
|
@ -0,0 +1 @@
|
|||
from .main import JumpServerPAM, SecretRequest
|
|
@ -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)
|
|
@ -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',
|
||||
)
|
|
@ -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')},
|
||||
},
|
||||
),
|
||||
]
|
|
@ -3,3 +3,4 @@ from .base import * # noqa
|
|||
from .automations import * # noqa
|
||||
from .template import * # noqa
|
||||
from .virtual import * # noqa
|
||||
from .service import * # noqa
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
|
|
|
@ -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()
|
|
@ -3,3 +3,4 @@ from .backup import *
|
|||
from .base import *
|
||||
from .template import *
|
||||
from .virtual import *
|
||||
from .service import *
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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')),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -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"))
|
||||
|
|
|
@ -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')},
|
||||
}
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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,))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -39,6 +39,7 @@ __all__ = [
|
|||
"BitChoices",
|
||||
"TreeChoices",
|
||||
"JSONManyToManyField",
|
||||
"RelatedManager",
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -25,7 +25,7 @@ __all__ = [
|
|||
'IDInFilterBackend', "CustomFilterBackend",
|
||||
"BaseFilterSet", 'IDNotFilterBackend',
|
||||
'NotOrRelFilterBackend', 'LabelFilterBackend',
|
||||
'RewriteOrderingFilter'
|
||||
'RewriteOrderingFilter', 'AttrRulesFilterBackend'
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "セッションの接続トレンド",
|
||||
|
|
|
@ -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": "会话连接趋势",
|
||||
|
|
|
@ -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": "會話指令",
|
||||
|
|
|
@ -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',
|
||||
),
|
||||
|
|
|
@ -29,7 +29,8 @@ __all__ = [
|
|||
"User",
|
||||
"UserPasswordHistory",
|
||||
"MFAMixin",
|
||||
"AuthMixin"
|
||||
"AuthMixin",
|
||||
"RoleMixin"
|
||||
]
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue