mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-12-15 16:42:34 +00:00
Compare commits
255 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb163b088f | ||
|
|
d5e7f06653 | ||
|
|
9560c4b05d | ||
|
|
81a5febfbc | ||
|
|
a09a59e7b3 | ||
|
|
a7d0e3ce70 | ||
|
|
6f762d7198 | ||
|
|
ddbcbb0d66 | ||
|
|
a1976e4f0b | ||
|
|
0eeb1e6f6d | ||
|
|
ae5487720c | ||
|
|
0d8386359e | ||
|
|
d338e53862 | ||
|
|
441c6a7f60 | ||
|
|
79a40f7ded | ||
|
|
b9c221c856 | ||
|
|
f6dada03bf | ||
|
|
d3d18b8f48 | ||
|
|
a2f23c2d81 | ||
|
|
f0df23a15a | ||
|
|
a6a4bb1c7d | ||
|
|
9ac3c6b120 | ||
|
|
005dd27701 | ||
|
|
ac6052546a | ||
|
|
0265adcc72 | ||
|
|
9654083662 | ||
|
|
08ff8fa285 | ||
|
|
f82f7eba2b | ||
|
|
a8cee26874 | ||
|
|
8080d36d90 | ||
|
|
3ed7477057 | ||
|
|
a3cddd5d34 | ||
|
|
26b3c60e5c | ||
|
|
b5dea38164 | ||
|
|
7addb881f6 | ||
|
|
9c395b674f | ||
|
|
b297ebe973 | ||
|
|
5c7bfcff1c | ||
|
|
76796f249d | ||
|
|
55a63477ed | ||
|
|
5942037d81 | ||
|
|
5882b8a682 | ||
|
|
34e75099a3 | ||
|
|
8fe84345e4 | ||
|
|
a31c3ccc30 | ||
|
|
e13e34098a | ||
|
|
e8653c74cd | ||
|
|
1433c35ff9 | ||
|
|
a237b5a63d | ||
|
|
2587c8693e | ||
|
|
dfe5e2bce3 | ||
|
|
91a34d1a88 | ||
|
|
1a05a942c2 | ||
|
|
30556023d1 | ||
|
|
aa022a02c1 | ||
|
|
433d829c29 | ||
|
|
3b507dc795 | ||
|
|
8233c69038 | ||
|
|
0fbc548c02 | ||
|
|
aa9ae14e46 | ||
|
|
04b35ba520 | ||
|
|
580d2cd80b | ||
|
|
da9136f7af | ||
|
|
1ce2706f20 | ||
|
|
bbdeba3659 | ||
|
|
3a26b9d102 | ||
|
|
ee757e261d | ||
|
|
f41e6db007 | ||
|
|
7eed7b32cc | ||
|
|
efb26132f6 | ||
|
|
572c5b6925 | ||
|
|
8a1cd7e2a9 | ||
|
|
c065f82d30 | ||
|
|
995c9a6c19 | ||
|
|
5ec970fab4 | ||
|
|
166745baf6 | ||
|
|
d320443c9f | ||
|
|
4bfa88f01f | ||
|
|
aedd8ba589 | ||
|
|
172b492bc3 | ||
|
|
c4280d259a | ||
|
|
b25ec559bb | ||
|
|
cd46c8c78e | ||
|
|
8839e6293b | ||
|
|
4f887b1b11 | ||
|
|
90840a4417 | ||
|
|
a18b9bad0a | ||
|
|
e1a238b778 | ||
|
|
ee44ae2e12 | ||
|
|
d77e84e6f8 | ||
|
|
2042c7a6e5 | ||
|
|
40aca26155 | ||
|
|
e18e76002c | ||
|
|
c77f02b295 | ||
|
|
3924ff0114 | ||
|
|
6a0264ad3b | ||
|
|
2d7349d596 | ||
|
|
c41a81c8d0 | ||
|
|
7ba19ab1a1 | ||
|
|
72247d1df3 | ||
|
|
faf82d7cfb | ||
|
|
4e8defc647 | ||
|
|
2f18208874 | ||
|
|
b37e8cdc3f | ||
|
|
5b960fc46b | ||
|
|
df51c82cfd | ||
|
|
e9deb6fc7a | ||
|
|
cca49fa9cd | ||
|
|
cfed849175 | ||
|
|
a7cc457f54 | ||
|
|
5996cedcd6 | ||
|
|
567c1b0124 | ||
|
|
2da541c127 | ||
|
|
307b739a03 | ||
|
|
ca5708988a | ||
|
|
90d84f4d69 | ||
|
|
758f418f63 | ||
|
|
a64ec8a1d2 | ||
|
|
60564d1b4f | ||
|
|
017710c056 | ||
|
|
a876a82a76 | ||
|
|
8423ae602f | ||
|
|
8e2471c1eb | ||
|
|
224a9fbdb3 | ||
|
|
797b184c7f | ||
|
|
b3632f6531 | ||
|
|
e3bc54e764 | ||
|
|
f0325c48df | ||
|
|
416d4bd0c3 | ||
|
|
10c877c120 | ||
|
|
b644c47173 | ||
|
|
45331dc9e8 | ||
|
|
8a565b9eef | ||
|
|
4eb7b50b52 | ||
|
|
fd64bd03b4 | ||
|
|
9c75147179 | ||
|
|
147e4cce94 | ||
|
|
d1e25e1fef | ||
|
|
af2ba07338 | ||
|
|
29b9adb684 | ||
|
|
64e0860d24 | ||
|
|
9934007397 | ||
|
|
4044a71aea | ||
|
|
9725f0c963 | ||
|
|
b017e68a56 | ||
|
|
9ca0eaf7ce | ||
|
|
94e60e180e | ||
|
|
8ed221ea5a | ||
|
|
42ebb1f82f | ||
|
|
9492518773 | ||
|
|
1cca9c10fb | ||
|
|
c4a6715eb8 | ||
|
|
4c31b5ec0f | ||
|
|
9fd7fa9339 | ||
|
|
a930f3aab3 | ||
|
|
5081fb5fe7 | ||
|
|
cb072123d6 | ||
|
|
761265dec5 | ||
|
|
89de111acc | ||
|
|
14327ee398 | ||
|
|
1b007c8c5c | ||
|
|
3222687aaa | ||
|
|
79994f5ddc | ||
|
|
8271492ec1 | ||
|
|
27560793f8 | ||
|
|
615929dd43 | ||
|
|
a1c1b128e9 | ||
|
|
fa2c70c6be | ||
|
|
46e119db1f | ||
|
|
0afff45bae | ||
|
|
31d219524b | ||
|
|
a20884e2ad | ||
|
|
eb6bddc599 | ||
|
|
8a8ed90eef | ||
|
|
75825f5baa | ||
|
|
0141fce27d | ||
|
|
3f9f9351f3 | ||
|
|
390b8693df | ||
|
|
dafc416783 | ||
|
|
04e46e4b1c | ||
|
|
ab1024fbf4 | ||
|
|
03afa4f974 | ||
|
|
edfca5eb24 | ||
|
|
02ca473492 | ||
|
|
484b75bb53 | ||
|
|
9c3fd59ef4 | ||
|
|
bbf3250161 | ||
|
|
8604b9019f | ||
|
|
966b4250b8 | ||
|
|
291f2b0e13 | ||
|
|
a1d15ef206 | ||
|
|
e76eec530f | ||
|
|
add4d8d2cd | ||
|
|
c6ece550a9 | ||
|
|
e3b620089a | ||
|
|
64f721875b | ||
|
|
02d3747c70 | ||
|
|
09494193ab | ||
|
|
702111f578 | ||
|
|
e08db7423f | ||
|
|
0c95faac04 | ||
|
|
534cbf1281 | ||
|
|
abe5fa9036 | ||
|
|
2a2f05e51c | ||
|
|
f460916e84 | ||
|
|
ad2cb233d7 | ||
|
|
0dbf035146 | ||
|
|
ea124fd0db | ||
|
|
83ff8dbf26 | ||
|
|
783c163324 | ||
|
|
3deced4ade | ||
|
|
63de4e1806 | ||
|
|
48d0c7b6cc | ||
|
|
20cc8a124f | ||
|
|
db050e405d | ||
|
|
e259d2a9e9 | ||
|
|
18c388f3a5 | ||
|
|
b088362ae3 | ||
|
|
c7f8ebb613 | ||
|
|
de2f9ae687 | ||
|
|
a806a2d3e6 | ||
|
|
7be76feeb0 | ||
|
|
f548abcb87 | ||
|
|
35c6b581e2 | ||
|
|
40b119786b | ||
|
|
ff9347e344 | ||
|
|
da0bbcee57 | ||
|
|
824d10ce93 | ||
|
|
1d6dcf9fb5 | ||
|
|
3325867f8f | ||
|
|
2f43aeee5d | ||
|
|
764d70ea4f | ||
|
|
ff6dbe67a6 | ||
|
|
7f36958683 | ||
|
|
fa5921cd86 | ||
|
|
540679df11 | ||
|
|
597b022905 | ||
|
|
ab34b9906e | ||
|
|
755fa8efa8 | ||
|
|
889713f00e | ||
|
|
c10436de47 | ||
|
|
333bb64b8b | ||
|
|
67a49dc5e9 | ||
|
|
8085db7acc | ||
|
|
6adeafd1d2 | ||
|
|
1faba95a48 | ||
|
|
ab07091eb8 | ||
|
|
f994f5d776 | ||
|
|
55fae1667d | ||
|
|
b3397c6aeb | ||
|
|
8ab4f6f004 | ||
|
|
5a37540eda | ||
|
|
1c1d507e34 | ||
|
|
9c40314edc | ||
|
|
acdde5a236 |
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
#### What this PR does / why we need it?
|
||||
|
||||
#### Summary of your change
|
||||
|
||||
#### Please indicate you've done the following:
|
||||
|
||||
- [ ] Made sure tests are passing and test coverage is added if needed.
|
||||
- [ ] Made sure commit message follow the rule of [Conventional Commits specification](https://www.conventionalcommits.org/).
|
||||
- [ ] Considered the docs impact and opened a new docs issue or PR with docs changes if needed.
|
||||
2
.github/workflows/release-drafter.yml
vendored
2
.github/workflows/release-drafter.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
run: |
|
||||
TAG=$(basename ${GITHUB_REF})
|
||||
VERSION=${TAG/v/}
|
||||
wget https://raw.githubusercontent.com/jumpserver/installer/v${VERSION}/quick_start.sh
|
||||
wget https://raw.githubusercontent.com/jumpserver/installer/master/quick_start.sh
|
||||
sed -i "s@Version=.*@Version=v${VERSION}@g" quick_start.sh
|
||||
echo "::set-output name=TAG::$TAG"
|
||||
echo "::set-output name=VERSION::$VERSION"
|
||||
|
||||
128
CODE_OF_CONDUCT.md
Normal file
128
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
25
CONTRIBUTING.md
Normal file
25
CONTRIBUTING.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Contributing
|
||||
|
||||
## Create pull request
|
||||
PR are always welcome, even if they only contain small fixes like typos or a few lines of code. If there will be a significant effort, please document it as an issue and get a discussion going before starting to work on it.
|
||||
|
||||
Please submit a PR broken down into small changes bit by bit. A PR consisting of a lot features and code changes may be hard to review. It is recommended to submit PRs in an incremental fashion.
|
||||
|
||||
This [development guideline](https://docs.jumpserver.org/zh/master/dev/rest_api/) contains information about repository structure, how to setup development environment, how to run it, and more.
|
||||
|
||||
Note: If you split your pull request to small changes, please make sure any of the changes goes to master will not break anything. Otherwise, it can not be merged until this feature complete.
|
||||
|
||||
## Report issues
|
||||
It is a great way to contribute by reporting an issue. Well-written and complete bug reports are always welcome! Please open an issue and follow the template to fill in required information.
|
||||
|
||||
Before opening any issue, please look up the existing issues to avoid submitting a duplication.
|
||||
If you find a match, you can "subscribe" to it to get notified on updates. If you have additional helpful information about the issue, please leave a comment.
|
||||
|
||||
When reporting issues, always include:
|
||||
|
||||
* Which version you are using.
|
||||
* Steps to reproduce the issue.
|
||||
* Snapshots or log files if needed
|
||||
|
||||
Because the issues are open to the public, when submitting files, be sure to remove any sensitive information, e.g. user name, password, IP address, and company name. You can
|
||||
replace those parts with "REDACTED" or other strings like "****".
|
||||
63
Dockerfile
63
Dockerfile
@@ -8,7 +8,6 @@ WORKDIR /opt/jumpserver
|
||||
ADD . .
|
||||
RUN cd utils && bash -ixeu build.sh
|
||||
|
||||
# 构建运行时环境
|
||||
FROM python:3.8-slim
|
||||
ARG PIP_MIRROR=https://pypi.douban.com/simple
|
||||
ENV PIP_MIRROR=$PIP_MIRROR
|
||||
@@ -17,38 +16,66 @@ ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
|
||||
COPY ./requirements/deb_requirements.txt ./requirements/deb_requirements.txt
|
||||
ARG BUILD_DEPENDENCIES=" \
|
||||
g++ \
|
||||
make \
|
||||
pkg-config"
|
||||
|
||||
ARG DEPENDENCIES=" \
|
||||
default-libmysqlclient-dev \
|
||||
freetds-dev \
|
||||
libpq-dev \
|
||||
libffi-dev \
|
||||
libldap2-dev \
|
||||
libsasl2-dev \
|
||||
libxml2-dev \
|
||||
libxmlsec1-dev \
|
||||
libxmlsec1-openssl \
|
||||
libaio-dev \
|
||||
sshpass"
|
||||
|
||||
ARG TOOLS=" \
|
||||
curl \
|
||||
default-mysql-client \
|
||||
iproute2 \
|
||||
iputils-ping \
|
||||
locales \
|
||||
procps \
|
||||
redis-tools \
|
||||
telnet \
|
||||
vim \
|
||||
wget"
|
||||
|
||||
RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
|
||||
&& sed -i 's/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
|
||||
&& apt update \
|
||||
&& apt -y install telnet iproute2 redis-tools default-mysql-client vim wget curl locales procps \
|
||||
&& apt -y install $(cat requirements/deb_requirements.txt) \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& apt -y install ${BUILD_DEPENDENCIES} \
|
||||
&& apt -y install ${DEPENDENCIES} \
|
||||
&& apt -y install ${TOOLS} \
|
||||
&& localedef -c -f UTF-8 -i zh_CN zh_CN.UTF-8 \
|
||||
&& cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
&& sed -i "s@# alias l@alias l@g" ~/.bashrc \
|
||||
&& echo "set mouse-=a" > ~/.vimrc
|
||||
|
||||
COPY ./requirements/requirements.txt ./requirements/requirements.txt
|
||||
RUN pip install --upgrade pip==20.2.4 setuptools==49.6.0 wheel==0.34.2 -i ${PIP_MIRROR} \
|
||||
&& pip install --no-cache-dir $(grep -E 'jms|jumpserver' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \
|
||||
&& pip install --no-cache-dir -r requirements/requirements.txt -i ${PIP_MIRROR} \
|
||||
&& rm -rf ~/.cache/pip
|
||||
|
||||
COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver
|
||||
RUN mkdir -p /root/.ssh/ \
|
||||
&& mkdir -p /root/.ssh/ \
|
||||
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config \
|
||||
&& sed -i "s@# alias l@alias l@g" ~/.bashrc \
|
||||
&& echo "set mouse-=a" > ~/.vimrc \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& mv /bin/sh /bin/sh.bak \
|
||||
&& ln -s /bin/bash /bin/sh
|
||||
|
||||
RUN mkdir -p /opt/jumpserver/oracle/ \
|
||||
&& wget https://download.jumpserver.org/public/instantclient-basiclite-linux.x64-21.1.0.0.0.tar > /dev/null \
|
||||
&& wget https://download.jumpserver.org/public/instantclient-basiclite-linux.x64-21.1.0.0.0.tar \
|
||||
&& tar xf instantclient-basiclite-linux.x64-21.1.0.0.0.tar -C /opt/jumpserver/oracle/ \
|
||||
&& echo "/opt/jumpserver/oracle/instantclient_21_1" > /etc/ld.so.conf.d/oracle-instantclient.conf \
|
||||
&& ldconfig \
|
||||
&& rm -f instantclient-basiclite-linux.x64-21.1.0.0.0.tar
|
||||
|
||||
RUN echo > config.yml
|
||||
COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver
|
||||
|
||||
RUN echo > config.yml \
|
||||
&& pip install --upgrade pip==20.2.4 setuptools==49.6.0 wheel==0.34.2 -i ${PIP_MIRROR} \
|
||||
&& pip install --no-cache-dir $(grep -E 'jms|jumpserver' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \
|
||||
&& pip install --no-cache-dir -r requirements/requirements.txt -i ${PIP_MIRROR} \
|
||||
&& rm -rf ~/.cache/pip
|
||||
|
||||
VOLUME /opt/jumpserver/data
|
||||
VOLUME /opt/jumpserver/logs
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
from common.permissions import IsOrgAdmin, HasQueryParamsUserAndIsCurrentOrgMember
|
||||
from common.drf.api import JMSBulkModelViewSet
|
||||
from ..models import LoginACL
|
||||
from .. import serializers
|
||||
from ..filters import LoginAclFilter
|
||||
|
||||
__all__ = ['LoginACLViewSet', ]
|
||||
__all__ = ['LoginACLViewSet']
|
||||
|
||||
|
||||
class LoginACLViewSet(JMSBulkModelViewSet):
|
||||
queryset = LoginACL.objects.all()
|
||||
filterset_class = LoginAclFilter
|
||||
search_fields = ('name',)
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.LoginACLSerializer
|
||||
|
||||
def get_permissions(self):
|
||||
if self.action in ["retrieve", "list"]:
|
||||
self.permission_classes = (IsOrgAdmin, HasQueryParamsUserAndIsCurrentOrgMember)
|
||||
return super().get_permissions()
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from common.permissions import IsOrgAdmin
|
||||
from .. import models, serializers
|
||||
|
||||
|
||||
@@ -10,5 +9,4 @@ class LoginAssetACLViewSet(OrgBulkModelViewSet):
|
||||
model = models.LoginAssetACL
|
||||
filterset_fields = ('name', )
|
||||
search_fields = filterset_fields
|
||||
permission_classes = (IsOrgAdmin, )
|
||||
serializer_class = serializers.LoginAssetACLSerializer
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.generics import CreateAPIView
|
||||
|
||||
from common.permissions import IsAppUser
|
||||
from common.utils import reverse, lazyproperty
|
||||
from orgs.utils import tmp_to_org
|
||||
from tickets.api import GenericTicketStatusRetrieveCloseAPI
|
||||
from ..models import LoginAssetACL
|
||||
from .. import serializers
|
||||
|
||||
__all__ = ['LoginAssetCheckAPI', 'LoginAssetConfirmStatusAPI']
|
||||
__all__ = ['LoginAssetCheckAPI']
|
||||
|
||||
|
||||
class LoginAssetCheckAPI(CreateAPIView):
|
||||
permission_classes = (IsAppUser,)
|
||||
serializer_class = serializers.LoginAssetCheckSerializer
|
||||
model = LoginAssetACL
|
||||
rbac_perms = {
|
||||
'POST': 'tickets.add_superticket'
|
||||
}
|
||||
|
||||
def get_queryset(self):
|
||||
return LoginAssetACL.objects.all()
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
is_need_confirm, response_data = self.check_if_need_confirm()
|
||||
@@ -46,7 +50,7 @@ class LoginAssetCheckAPI(CreateAPIView):
|
||||
org_id=self.serializer.org.id
|
||||
)
|
||||
confirm_status_url = reverse(
|
||||
view_name='api-acls:login-asset-confirm-status',
|
||||
view_name='api-tickets:super-ticket-status',
|
||||
kwargs={'pk': str(ticket.id)}
|
||||
)
|
||||
ticket_detail_url = reverse(
|
||||
@@ -71,6 +75,3 @@ class LoginAssetCheckAPI(CreateAPIView):
|
||||
serializer.is_valid(raise_exception=True)
|
||||
return serializer
|
||||
|
||||
|
||||
class LoginAssetConfirmStatusAPI(GenericTicketStatusRetrieveCloseAPI):
|
||||
pass
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
class AclsConfig(AppConfig):
|
||||
name = 'acls'
|
||||
verbose_name = _('Acls')
|
||||
|
||||
21
apps/acls/migrations/0003_auto_20211130_1037.py
Normal file
21
apps/acls/migrations/0003_auto_20211130_1037.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# Generated by Django 3.1.13 on 2021-11-30 02:37
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('acls', '0002_auto_20210926_1047'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='loginacl',
|
||||
options={'ordering': ('priority', '-date_updated', 'name'), 'verbose_name': 'Login acl'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='loginassetacl',
|
||||
options={'ordering': ('priority', '-date_updated', 'name'), 'verbose_name': 'Login asset acl'},
|
||||
),
|
||||
]
|
||||
@@ -12,7 +12,6 @@ router.register(r'login-asset-acls', api.LoginAssetACLViewSet, 'login-asset-acl'
|
||||
|
||||
urlpatterns = [
|
||||
path('login-asset/check/', api.LoginAssetCheckAPI.as_view(), name='login-asset-check'),
|
||||
path('login-asset-confirm/<uuid:pk>/status/', api.LoginAssetConfirmStatusAPI.as_view(), name='login-asset-confirm-status')
|
||||
]
|
||||
|
||||
urlpatterns += router.urls
|
||||
|
||||
@@ -6,8 +6,10 @@ from django.db.models import F, Q
|
||||
|
||||
from common.drf.filters import BaseFilterSet
|
||||
from common.drf.api import JMSBulkModelViewSet
|
||||
from rbac.permissions import RBACPermission
|
||||
from assets.models import SystemUser
|
||||
from ..models import Account
|
||||
from ..hands import IsOrgAdminOrAppUser, IsOrgAdmin, NeedMFAVerify
|
||||
from ..hands import NeedMFAVerify
|
||||
from .. import serializers
|
||||
|
||||
|
||||
@@ -31,7 +33,8 @@ class AccountFilterSet(BaseFilterSet):
|
||||
username = self.get_query_param('username')
|
||||
if not username:
|
||||
return qs
|
||||
qs = qs.filter(Q(username=username) | Q(systemuser__username=username)).distinct()
|
||||
q = Q(username=username) | Q(systemuser__username=username)
|
||||
qs = qs.filter(q).distinct()
|
||||
return qs
|
||||
|
||||
|
||||
@@ -41,14 +44,21 @@ class ApplicationAccountViewSet(JMSBulkModelViewSet):
|
||||
filterset_class = AccountFilterSet
|
||||
filterset_fields = ['username', 'app_display', 'type', 'category', 'app']
|
||||
serializer_class = serializers.AppAccountSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = Account.get_queryset()
|
||||
return queryset
|
||||
|
||||
|
||||
class SystemUserAppRelationViewSet(ApplicationAccountViewSet):
|
||||
perm_model = SystemUser
|
||||
|
||||
|
||||
class ApplicationAccountSecretViewSet(ApplicationAccountViewSet):
|
||||
serializer_class = serializers.AppAccountSecretSerializer
|
||||
permission_classes = [IsOrgAdminOrAppUser, NeedMFAVerify]
|
||||
permission_classes = [RBACPermission, NeedMFAVerify]
|
||||
http_method_names = ['get', 'options']
|
||||
rbac_perms = {
|
||||
'retrieve': 'applications.view_applicationaccountsecret',
|
||||
'list': 'applications.view_applicationaccountsecret',
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ from rest_framework.response import Response
|
||||
|
||||
from common.tree import TreeNodeSerializer
|
||||
from common.mixins.api import SuggestionMixin
|
||||
from ..hands import IsOrgAdminOrAppUser
|
||||
from .. import serializers
|
||||
from ..models import Application
|
||||
|
||||
@@ -18,16 +17,19 @@ class ApplicationViewSet(SuggestionMixin, OrgBulkModelViewSet):
|
||||
model = Application
|
||||
filterset_fields = {
|
||||
'name': ['exact'],
|
||||
'category': ['exact'],
|
||||
'category': ['exact', 'in'],
|
||||
'type': ['exact', 'in'],
|
||||
}
|
||||
search_fields = ('name', 'type', 'category')
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_classes = {
|
||||
'default': serializers.AppSerializer,
|
||||
'get_tree': TreeNodeSerializer,
|
||||
'suggestion': serializers.MiniAppSerializer
|
||||
}
|
||||
rbac_perms = {
|
||||
'get_tree': 'applications.view_application',
|
||||
'match': 'applications.match_application'
|
||||
}
|
||||
|
||||
@action(methods=['GET'], detail=False, url_path='tree')
|
||||
def get_tree(self, request, *args, **kwargs):
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
#
|
||||
|
||||
from orgs.mixins import generics
|
||||
from ..hands import IsAppUser
|
||||
from .. import models
|
||||
from ..serializers import RemoteAppConnectionInfoSerializer
|
||||
from ..permissions import IsRemoteApp
|
||||
|
||||
|
||||
__all__ = [
|
||||
@@ -15,5 +13,4 @@ __all__ = [
|
||||
|
||||
class RemoteAppConnectionInfoApi(generics.RetrieveAPIView):
|
||||
model = models.Application
|
||||
permission_classes = (IsAppUser, IsRemoteApp)
|
||||
serializer_class = RemoteAppConnectionInfoSerializer
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ApplicationsConfig(AppConfig):
|
||||
name = 'applications'
|
||||
verbose_name = _('Applications')
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
from django.db.models import TextChoices
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
class AppCategory(TextChoices):
|
||||
class AppCategory(models.TextChoices):
|
||||
db = 'db', _('Database')
|
||||
remote_app = 'remote_app', _('Remote app')
|
||||
cloud = 'cloud', 'Cloud'
|
||||
@@ -13,15 +13,20 @@ class AppCategory(TextChoices):
|
||||
def get_label(cls, category):
|
||||
return dict(cls.choices).get(category, '')
|
||||
|
||||
@classmethod
|
||||
def is_xpack(cls, category):
|
||||
return category in ['remote_app']
|
||||
|
||||
class AppType(TextChoices):
|
||||
|
||||
class AppType(models.TextChoices):
|
||||
# db category
|
||||
mysql = 'mysql', 'MySQL'
|
||||
redis = 'redis', 'Redis'
|
||||
mariadb = 'mariadb', 'MariaDB'
|
||||
oracle = 'oracle', 'Oracle'
|
||||
pgsql = 'postgresql', 'PostgreSQL'
|
||||
mariadb = 'mariadb', 'MariaDB'
|
||||
sqlserver = 'sqlserver', 'SQLServer'
|
||||
redis = 'redis', 'Redis'
|
||||
mongodb = 'mongodb', 'MongoDB'
|
||||
|
||||
# remote-app category
|
||||
chrome = 'chrome', 'Chrome'
|
||||
@@ -36,9 +41,13 @@ class AppType(TextChoices):
|
||||
def category_types_mapper(cls):
|
||||
return {
|
||||
AppCategory.db: [
|
||||
cls.mysql, cls.oracle, cls.redis, cls.pgsql, cls.mariadb, cls.sqlserver
|
||||
cls.mysql, cls.oracle, cls.pgsql, cls.mariadb,
|
||||
cls.sqlserver, cls.redis, cls.mongodb
|
||||
],
|
||||
AppCategory.remote_app: [
|
||||
cls.chrome, cls.mysql_workbench,
|
||||
cls.vmware_client, cls.custom
|
||||
],
|
||||
AppCategory.remote_app: [cls.chrome, cls.mysql_workbench, cls.vmware_client, cls.custom],
|
||||
AppCategory.cloud: [cls.k8s]
|
||||
}
|
||||
|
||||
@@ -65,3 +74,12 @@ class AppType(TextChoices):
|
||||
@classmethod
|
||||
def cloud_types(cls):
|
||||
return [tp.value for tp in cls.category_types_mapper()[AppCategory.cloud]]
|
||||
|
||||
@classmethod
|
||||
def is_xpack(cls, tp):
|
||||
tp_category_mapper = cls.type_category_mapper()
|
||||
category = tp_category_mapper[tp]
|
||||
|
||||
if AppCategory.is_xpack(category):
|
||||
return True
|
||||
return tp in ['oracle', 'postgresql', 'sqlserver']
|
||||
|
||||
@@ -11,5 +11,5 @@
|
||||
"""
|
||||
|
||||
|
||||
from common.permissions import IsAppUser, IsOrgAdmin, IsValidUser, IsOrgAdminOrAppUser, NeedMFAVerify
|
||||
from common.permissions import NeedMFAVerify
|
||||
from users.models import User, UserGroup
|
||||
|
||||
25
apps/applications/migrations/0017_auto_20220217_2135.py
Normal file
25
apps/applications/migrations/0017_auto_20220217_2135.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 3.1.13 on 2022-02-17 13:35
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('applications', '0016_auto_20220118_1455'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='account',
|
||||
options={'permissions': [('view_applicationaccountsecret', 'Can view application account secret'), ('change_appplicationaccountsecret', 'Can change application account secret')], 'verbose_name': 'Application account'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='applicationuser',
|
||||
options={'verbose_name': 'Application user'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='historicalaccount',
|
||||
options={'get_latest_by': 'history_date', 'ordering': ('-history_date', '-history_id'), 'verbose_name': 'historical Application account'},
|
||||
),
|
||||
]
|
||||
18
apps/applications/migrations/0018_auto_20220223_1539.py
Normal file
18
apps/applications/migrations/0018_auto_20220223_1539.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.13 on 2022-02-23 07:39
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('applications', '0017_auto_20220217_2135'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='application',
|
||||
name='type',
|
||||
field=models.CharField(choices=[('mysql', 'MySQL'), ('oracle', 'Oracle'), ('postgresql', 'PostgreSQL'), ('mariadb', 'MariaDB'), ('sqlserver', 'SQLServer'), ('redis', 'Redis'), ('mongodb', 'MongoDB'), ('chrome', 'Chrome'), ('mysql_workbench', 'MySQL Workbench'), ('vmware_client', 'vSphere Client'), ('custom', 'Custom'), ('k8s', 'Kubernetes')], max_length=16, verbose_name='Type'),
|
||||
),
|
||||
]
|
||||
17
apps/applications/migrations/0019_auto_20220310_1853.py
Normal file
17
apps/applications/migrations/0019_auto_20220310_1853.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 3.1.14 on 2022-03-10 10:53
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('applications', '0018_auto_20220223_1539'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='application',
|
||||
options={'ordering': ('name',), 'permissions': [('match_application', 'Can match application')], 'verbose_name': 'Application'},
|
||||
),
|
||||
]
|
||||
18
apps/applications/migrations/0020_auto_20220316_2028.py
Normal file
18
apps/applications/migrations/0020_auto_20220316_2028.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.14 on 2022-03-16 12:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('applications', '0019_auto_20220310_1853'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='application',
|
||||
name='type',
|
||||
field=models.CharField(choices=[('mysql', 'MySQL'), ('mariadb', 'MariaDB'), ('oracle', 'Oracle'), ('postgresql', 'PostgreSQL'), ('sqlserver', 'SQLServer'), ('redis', 'Redis'), ('mongodb', 'MongoDB'), ('chrome', 'Chrome'), ('mysql_workbench', 'MySQL Workbench'), ('vmware_client', 'vSphere Client'), ('custom', 'Custom'), ('k8s', 'Kubernetes')], max_length=16, verbose_name='Type'),
|
||||
),
|
||||
]
|
||||
@@ -20,8 +20,12 @@ class Account(BaseUser):
|
||||
auth_attrs = ['username', 'password', 'private_key', 'public_key']
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Account')
|
||||
verbose_name = _('Application account')
|
||||
unique_together = [('username', 'app', 'systemuser')]
|
||||
permissions = [
|
||||
('view_applicationaccountsecret', _('Can view application account secret')),
|
||||
('change_appplicationaccountsecret', _('Can change application account secret')),
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -3,10 +3,12 @@ from urllib.parse import urlencode, parse_qsl
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.conf import settings
|
||||
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
from common.mixins import CommonModelMixin
|
||||
from common.tree import TreeNode
|
||||
from common.utils import is_uuid
|
||||
from assets.models import Asset, SystemUser
|
||||
|
||||
from ..utils import KubernetesTree
|
||||
@@ -79,6 +81,8 @@ class ApplicationTreeNodeMixin:
|
||||
nodes = []
|
||||
categories = const.AppType.category_types_mapper().keys()
|
||||
for category in categories:
|
||||
if not settings.XPACK_ENABLED and const.AppCategory.is_xpack(category):
|
||||
continue
|
||||
i = cls.create_tree_id(pid, 'category', category.value)
|
||||
node = cls.create_choice_node(
|
||||
category, i, pid=pid, tp='category',
|
||||
@@ -97,6 +101,11 @@ class ApplicationTreeNodeMixin:
|
||||
type_category_mapper = const.AppType.type_category_mapper()
|
||||
types = const.AppType.type_category_mapper().keys()
|
||||
for tp in types:
|
||||
# TODO: Temporary exclude mongodb
|
||||
if tp == const.AppType.mongodb:
|
||||
continue
|
||||
if not settings.XPACK_ENABLED and const.AppType.is_xpack(tp):
|
||||
continue
|
||||
category = type_category_mapper.get(tp)
|
||||
pid = cls.create_tree_id(pid, 'category', category.value)
|
||||
i = cls.create_tree_id(pid, 'type', tp.value)
|
||||
@@ -155,6 +164,8 @@ class ApplicationTreeNodeMixin:
|
||||
|
||||
# 应用的节点
|
||||
for app in queryset:
|
||||
if not settings.XPACK_ENABLED and const.AppType.is_xpack(app.type):
|
||||
continue
|
||||
node = app.as_tree_node(root_node.id)
|
||||
tree_nodes.append(node)
|
||||
return tree_nodes
|
||||
@@ -219,6 +230,9 @@ class Application(CommonModelMixin, OrgModelMixin, ApplicationTreeNodeMixin):
|
||||
verbose_name = _('Application')
|
||||
unique_together = [('org_id', 'name')]
|
||||
ordering = ('name',)
|
||||
permissions = [
|
||||
('match_application', _('Can match application')),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
category_display = self.get_category_display()
|
||||
@@ -254,14 +268,15 @@ class Application(CommonModelMixin, OrgModelMixin, ApplicationTreeNodeMixin):
|
||||
'parameters': parameters
|
||||
}
|
||||
|
||||
def get_remote_app_asset(self):
|
||||
def get_remote_app_asset(self, raise_exception=True):
|
||||
asset_id = self.attrs.get('asset')
|
||||
if not asset_id:
|
||||
if is_uuid(asset_id):
|
||||
return Asset.objects.filter(id=asset_id).first()
|
||||
if raise_exception:
|
||||
raise ValueError("Remote App not has asset attr")
|
||||
asset = Asset.objects.filter(id=asset_id).first()
|
||||
return asset
|
||||
|
||||
|
||||
class ApplicationUser(SystemUser):
|
||||
class Meta:
|
||||
proxy = True
|
||||
verbose_name = _('Application user')
|
||||
|
||||
@@ -119,7 +119,8 @@ class AppAccountSerializer(AppSerializerMixin, AuthSerializerMixin, BulkOrgResou
|
||||
'username': {'default': '', 'required': False},
|
||||
'password': {'write_only': True},
|
||||
'app_display': {'label': _('Application display')},
|
||||
'systemuser_display': {'label': _('System User')}
|
||||
'systemuser_display': {'label': _('System User')},
|
||||
'account': {'label': _('account')}
|
||||
}
|
||||
use_model_bulk_create = True
|
||||
model_bulk_create_kwargs = {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
|
||||
from .mysql import *
|
||||
from .redis import *
|
||||
from .mariadb import *
|
||||
from .oracle import *
|
||||
from .pgsql import *
|
||||
from .sqlserver import *
|
||||
from .redis import *
|
||||
from .mongodb import *
|
||||
|
||||
from .chrome import *
|
||||
from .mysql_workbench import *
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
from rest_framework import serializers
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from ..application_category import DBSerializer
|
||||
|
||||
__all__ = ['MongoDBSerializer']
|
||||
|
||||
|
||||
class MongoDBSerializer(DBSerializer):
|
||||
port = serializers.IntegerField(default=27017, label=_('Port'), allow_null=True)
|
||||
|
||||
@@ -25,11 +25,12 @@ category_serializer_classes_mapping = {
|
||||
type_serializer_classes_mapping = {
|
||||
# db
|
||||
const.AppType.mysql.value: application_type.MySQLSerializer,
|
||||
const.AppType.redis.value: application_type.RedisSerializer,
|
||||
const.AppType.mariadb.value: application_type.MariaDBSerializer,
|
||||
const.AppType.oracle.value: application_type.OracleSerializer,
|
||||
const.AppType.pgsql.value: application_type.PostgreSerializer,
|
||||
const.AppType.sqlserver.value: application_type.SQLServerSerializer,
|
||||
const.AppType.redis.value: application_type.RedisSerializer,
|
||||
const.AppType.mongodb.value: application_type.MongoDBSerializer,
|
||||
# cloud
|
||||
const.AppType.k8s.value: application_type.K8SSerializer
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ app_name = 'applications'
|
||||
router = BulkRouter()
|
||||
router.register(r'applications', api.ApplicationViewSet, 'application')
|
||||
router.register(r'accounts', api.ApplicationAccountViewSet, 'application-account')
|
||||
router.register(r'system-users-apps-relations', api.SystemUserAppRelationViewSet, 'system-users-apps-relation')
|
||||
router.register(r'account-secrets', api.ApplicationAccountSecretViewSet, 'application-account-secret')
|
||||
|
||||
|
||||
|
||||
@@ -10,4 +10,4 @@ from .domain import *
|
||||
from .cmd_filter import *
|
||||
from .gathered_user import *
|
||||
from .favorite_asset import *
|
||||
from .backup import *
|
||||
from .account_backup import *
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from rest_framework import status, mixins, viewsets
|
||||
from rest_framework import status, viewsets
|
||||
from rest_framework.response import Response
|
||||
|
||||
from common.permissions import IsOrgAdmin
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
|
||||
from .. import serializers
|
||||
from ..tasks import execute_account_backup_plan
|
||||
from ..models import (
|
||||
@@ -24,17 +22,13 @@ class AccountBackupPlanViewSet(OrgBulkModelViewSet):
|
||||
ordering_fields = ('name',)
|
||||
ordering = ('name',)
|
||||
serializer_class = serializers.AccountBackupPlanSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
|
||||
class AccountBackupPlanExecutionViewSet(
|
||||
mixins.CreateModelMixin, mixins.ListModelMixin,
|
||||
mixins.RetrieveModelMixin, viewsets.GenericViewSet
|
||||
):
|
||||
class AccountBackupPlanExecutionViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = serializers.AccountBackupPlanExecutionSerializer
|
||||
search_fields = ('trigger',)
|
||||
filterset_fields = ('trigger', 'plan_id')
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
http_method_names = ['get', 'post', 'options']
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = AccountBackupPlanExecution.objects.all()
|
||||
@@ -1,13 +1,14 @@
|
||||
from django.db.models import F, Q
|
||||
from rest_framework.decorators import action
|
||||
from django_filters import rest_framework as filters
|
||||
from rest_framework.response import Response
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django_filters import rest_framework as filters
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.generics import CreateAPIView
|
||||
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, NeedMFAVerify
|
||||
from rbac.permissions import RBACPermission
|
||||
from common.drf.filters import BaseFilterSet
|
||||
from common.permissions import NeedMFAVerify
|
||||
from ..tasks.account_connectivity import test_accounts_connectivity_manual
|
||||
from ..models import AuthBook, Node
|
||||
from .. import serializers
|
||||
@@ -26,6 +27,7 @@ class AccountFilterSet(BaseFilterSet):
|
||||
qs = super().qs
|
||||
qs = self.filter_username(qs)
|
||||
qs = self.filter_node(qs)
|
||||
qs = qs.distinct()
|
||||
return qs
|
||||
|
||||
def filter_username(self, qs):
|
||||
@@ -61,7 +63,10 @@ class AccountViewSet(OrgBulkModelViewSet):
|
||||
'default': serializers.AccountSerializer,
|
||||
'verify_account': serializers.AssetTaskSerializer
|
||||
}
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
rbac_perms = {
|
||||
'verify_account': 'assets.test_authbook',
|
||||
'partial_update': 'assets.change_assetaccountsecret',
|
||||
}
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = AuthBook.get_queryset()
|
||||
@@ -81,17 +86,23 @@ class AccountSecretsViewSet(AccountViewSet):
|
||||
serializer_classes = {
|
||||
'default': serializers.AccountSecretSerializer
|
||||
}
|
||||
permission_classes = (IsOrgAdmin, NeedMFAVerify)
|
||||
http_method_names = ['get']
|
||||
permission_classes = [RBACPermission, NeedMFAVerify]
|
||||
rbac_perms = {
|
||||
'list': 'assets.view_assetaccountsecret',
|
||||
'retrieve': 'assets.view_assetaccountsecret',
|
||||
}
|
||||
|
||||
|
||||
class AccountTaskCreateAPI(CreateAPIView):
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.AccountTaskSerializer
|
||||
filterset_fields = AccountViewSet.filterset_fields
|
||||
search_fields = AccountViewSet.search_fields
|
||||
filterset_class = AccountViewSet.filterset_class
|
||||
|
||||
def check_permissions(self, request):
|
||||
return request.user.has_perm('assets.test_assetconnectivity')
|
||||
|
||||
def get_accounts(self):
|
||||
queryset = AuthBook.objects.all()
|
||||
queryset = self.filter_queryset(queryset)
|
||||
@@ -108,5 +119,4 @@ class AccountTaskCreateAPI(CreateAPIView):
|
||||
def get_exception_handler(self):
|
||||
def handler(e, context):
|
||||
return Response({"error": str(e)}, status=400)
|
||||
|
||||
return handler
|
||||
|
||||
@@ -2,9 +2,9 @@ from django.db.models import Count
|
||||
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from common.utils import get_logger
|
||||
from ..hands import IsOrgAdmin
|
||||
from ..models import SystemUser
|
||||
from .. import serializers
|
||||
from rbac.permissions import RBACPermission
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
@@ -20,7 +20,7 @@ class AdminUserViewSet(OrgBulkModelViewSet):
|
||||
filterset_fields = ("name", "username")
|
||||
search_fields = filterset_fields
|
||||
serializer_class = serializers.AdminUserSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
permission_classes = (RBACPermission,)
|
||||
ordering_fields = ('name',)
|
||||
ordering = ('name', )
|
||||
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from assets.api import FilterAssetByNodeMixin
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
from rest_framework.generics import RetrieveAPIView, ListAPIView
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.db.models import Q
|
||||
|
||||
from common.utils import get_logger, get_object_or_none
|
||||
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsSuperUser
|
||||
from common.mixins.api import SuggestionMixin
|
||||
from users.models import User, UserGroup
|
||||
from users.serializers import UserSerializer, UserGroupSerializer
|
||||
@@ -17,7 +15,8 @@ from perms.serializers import AssetPermissionSerializer
|
||||
from perms.filters import AssetPermissionFilter
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from orgs.mixins import generics
|
||||
from ..models import Asset, Node, Platform
|
||||
from assets.api import FilterAssetByNodeMixin
|
||||
from ..models import Asset, Node, Platform, Gateway
|
||||
from .. import serializers
|
||||
from ..tasks import (
|
||||
update_assets_hardware_info_manual, test_assets_connectivity_manual,
|
||||
@@ -55,7 +54,9 @@ class AssetViewSet(SuggestionMixin, FilterAssetByNodeMixin, OrgBulkModelViewSet)
|
||||
'default': serializers.AssetSerializer,
|
||||
'suggestion': serializers.MiniAssetSerializer
|
||||
}
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
rbac_perms = {
|
||||
'match': 'assets.match_asset'
|
||||
}
|
||||
extra_filter_backends = [FilterAssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend]
|
||||
|
||||
def set_assets_node(self, assets):
|
||||
@@ -76,8 +77,10 @@ class AssetViewSet(SuggestionMixin, FilterAssetByNodeMixin, OrgBulkModelViewSet)
|
||||
|
||||
class AssetPlatformRetrieveApi(RetrieveAPIView):
|
||||
queryset = Platform.objects.all()
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.PlatformSerializer
|
||||
rbac_perms = {
|
||||
'retrieve': 'assets.view_gateway'
|
||||
}
|
||||
|
||||
def get_object(self):
|
||||
asset_pk = self.kwargs.get('pk')
|
||||
@@ -87,16 +90,10 @@ class AssetPlatformRetrieveApi(RetrieveAPIView):
|
||||
|
||||
class AssetPlatformViewSet(ModelViewSet):
|
||||
queryset = Platform.objects.all()
|
||||
permission_classes = (IsSuperUser,)
|
||||
serializer_class = serializers.PlatformSerializer
|
||||
filterset_fields = ['name', 'base']
|
||||
search_fields = ['name']
|
||||
|
||||
def get_permissions(self):
|
||||
if self.request.method.lower() in ['get', 'options']:
|
||||
self.permission_classes = (IsOrgAdmin,)
|
||||
return super().get_permissions()
|
||||
|
||||
def check_object_permissions(self, request, obj):
|
||||
if request.method.lower() in ['delete', 'put', 'patch'] and obj.internal:
|
||||
self.permission_denied(
|
||||
@@ -131,7 +128,6 @@ class AssetsTaskMixin:
|
||||
class AssetTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
|
||||
model = Asset
|
||||
serializer_class = serializers.AssetTaskSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
pk = self.kwargs.get('pk')
|
||||
@@ -139,11 +135,26 @@ class AssetTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
|
||||
request.data['assets'] = [pk]
|
||||
return super().create(request, *args, **kwargs)
|
||||
|
||||
def check_permissions(self, request):
|
||||
action = request.data.get('action')
|
||||
action_perm_require = {
|
||||
'refresh': 'assets.refresh_assethardwareinfo',
|
||||
'push_system_user': 'assets.push_assetsystemuser',
|
||||
'test': 'assets.test_assetconnectivity',
|
||||
'test_system_user': 'assets.test_assetconnectivity'
|
||||
}
|
||||
perm_required = action_perm_require.get(action)
|
||||
has = self.request.user.has_perm(perm_required)
|
||||
|
||||
if not has:
|
||||
self.permission_denied(request)
|
||||
|
||||
def perform_asset_task(self, serializer):
|
||||
data = serializer.validated_data
|
||||
action = data['action']
|
||||
if action not in ['push_system_user', 'test_system_user']:
|
||||
return
|
||||
|
||||
asset = data['asset']
|
||||
system_users = data.get('system_users')
|
||||
if not system_users:
|
||||
@@ -166,24 +177,37 @@ class AssetTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
|
||||
class AssetsTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
|
||||
model = Asset
|
||||
serializer_class = serializers.AssetsTaskSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
def check_permissions(self, request):
|
||||
action = request.data.get('action')
|
||||
action_perm_require = {
|
||||
'refresh': 'assets.refresh_assethardwareinfo',
|
||||
}
|
||||
perm_required = action_perm_require.get(action)
|
||||
has = self.request.user.has_perm(perm_required)
|
||||
if not has:
|
||||
self.permission_denied(request)
|
||||
|
||||
|
||||
class AssetGatewayListApi(generics.ListAPIView):
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.GatewayWithAuthSerializer
|
||||
rbac_perms = {
|
||||
'list': 'assets.view_gateway'
|
||||
}
|
||||
|
||||
def get_queryset(self):
|
||||
asset_id = self.kwargs.get('pk')
|
||||
asset = get_object_or_404(Asset, pk=asset_id)
|
||||
if not asset.domain:
|
||||
return []
|
||||
return Gateway.objects.none()
|
||||
queryset = asset.domain.gateways.filter(protocol='ssh')
|
||||
return queryset
|
||||
|
||||
|
||||
class BaseAssetPermUserOrUserGroupListApi(ListAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
rbac_perms = {
|
||||
'GET': 'perms.view_assetpermission'
|
||||
}
|
||||
|
||||
def get_object(self):
|
||||
asset_id = self.kwargs.get('pk')
|
||||
@@ -201,6 +225,9 @@ class AssetPermUserListApi(BaseAssetPermUserOrUserGroupListApi):
|
||||
filterset_class = UserFilter
|
||||
search_fields = ('username', 'email', 'name', 'id', 'source', 'role')
|
||||
serializer_class = UserSerializer
|
||||
rbac_perms = {
|
||||
'GET': 'perms.view_assetpermission'
|
||||
}
|
||||
|
||||
def get_queryset(self):
|
||||
perms = self.get_asset_related_perms()
|
||||
@@ -220,11 +247,13 @@ class AssetPermUserGroupListApi(BaseAssetPermUserOrUserGroupListApi):
|
||||
|
||||
|
||||
class BaseAssetPermUserOrUserGroupPermissionsListApiMixin(generics.ListAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
model = AssetPermission
|
||||
serializer_class = AssetPermissionSerializer
|
||||
filterset_class = AssetPermissionFilter
|
||||
search_fields = ('name',)
|
||||
rbac_perms = {
|
||||
'list': 'perms.view_assetpermission'
|
||||
}
|
||||
|
||||
def get_object(self):
|
||||
asset_id = self.kwargs.get('pk')
|
||||
|
||||
@@ -8,14 +8,11 @@ from django.shortcuts import get_object_or_404
|
||||
from common.utils import reverse
|
||||
from common.utils import lazyproperty
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from tickets.api import GenericTicketStatusRetrieveCloseAPI
|
||||
from ..hands import IsOrgAdmin, IsAppUser
|
||||
from ..models import CommandFilter, CommandFilterRule
|
||||
from .. import serializers
|
||||
|
||||
__all__ = [
|
||||
'CommandFilterViewSet', 'CommandFilterRuleViewSet', 'CommandConfirmAPI',
|
||||
'CommandConfirmStatusAPI'
|
||||
]
|
||||
|
||||
|
||||
@@ -23,7 +20,6 @@ class CommandFilterViewSet(OrgBulkModelViewSet):
|
||||
model = CommandFilter
|
||||
filterset_fields = ("name",)
|
||||
search_fields = filterset_fields
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.CommandFilterSerializer
|
||||
|
||||
|
||||
@@ -31,7 +27,6 @@ class CommandFilterRuleViewSet(OrgBulkModelViewSet):
|
||||
model = CommandFilterRule
|
||||
filterset_fields = ('content',)
|
||||
search_fields = filterset_fields
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.CommandFilterRuleSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
@@ -43,8 +38,10 @@ class CommandFilterRuleViewSet(OrgBulkModelViewSet):
|
||||
|
||||
|
||||
class CommandConfirmAPI(CreateAPIView):
|
||||
permission_classes = (IsAppUser,)
|
||||
serializer_class = serializers.CommandConfirmSerializer
|
||||
rbac_perms = {
|
||||
'POST': 'tickets.add_superticket'
|
||||
}
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
ticket = self.create_command_confirm_ticket()
|
||||
@@ -56,14 +53,14 @@ class CommandConfirmAPI(CreateAPIView):
|
||||
run_command=self.serializer.data.get('run_command'),
|
||||
session=self.serializer.session,
|
||||
cmd_filter_rule=self.serializer.cmd_filter_rule,
|
||||
org_id=self.serializer.org.id
|
||||
org_id=self.serializer.org.id,
|
||||
)
|
||||
return ticket
|
||||
|
||||
@staticmethod
|
||||
def get_response_data(ticket):
|
||||
confirm_status_url = reverse(
|
||||
view_name='api-assets:command-confirm-status',
|
||||
view_name='api-tickets:super-ticket-status',
|
||||
kwargs={'pk': str(ticket.id)}
|
||||
)
|
||||
ticket_detail_url = reverse(
|
||||
@@ -86,6 +83,3 @@ class CommandConfirmAPI(CreateAPIView):
|
||||
serializer.is_valid(raise_exception=True)
|
||||
return serializer
|
||||
|
||||
|
||||
class CommandConfirmStatusAPI(GenericTicketStatusRetrieveCloseAPI):
|
||||
pass
|
||||
|
||||
@@ -6,7 +6,6 @@ from rest_framework.views import APIView, Response
|
||||
from rest_framework.serializers import ValidationError
|
||||
|
||||
from common.utils import get_logger
|
||||
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from ..models import Domain, Gateway
|
||||
from .. import serializers
|
||||
@@ -20,7 +19,6 @@ class DomainViewSet(OrgBulkModelViewSet):
|
||||
model = Domain
|
||||
filterset_fields = ("name", )
|
||||
search_fields = filterset_fields
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.DomainSerializer
|
||||
ordering_fields = ('name',)
|
||||
ordering = ('name', )
|
||||
@@ -35,13 +33,15 @@ class GatewayViewSet(OrgBulkModelViewSet):
|
||||
model = Gateway
|
||||
filterset_fields = ("domain__name", "name", "username", "ip", "domain")
|
||||
search_fields = ("domain__name", "name", "username", "ip")
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.GatewaySerializer
|
||||
|
||||
|
||||
class GatewayTestConnectionApi(SingleObjectMixin, APIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
queryset = Gateway.objects.all()
|
||||
object = None
|
||||
rbac_perms = {
|
||||
'POST': 'assets.test_gateway'
|
||||
}
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object(Gateway.objects.all())
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
from orgs.mixins.api import OrgModelViewSet
|
||||
from assets.models import GatheredUser
|
||||
from common.permissions import IsOrgAdmin
|
||||
|
||||
from ..serializers import GatheredUserSerializer
|
||||
from ..filters import AssetRelatedByNodeFilterBackend
|
||||
@@ -15,7 +14,6 @@ __all__ = ['GatheredUserViewSet']
|
||||
class GatheredUserViewSet(OrgModelViewSet):
|
||||
model = GatheredUser
|
||||
serializer_class = GatheredUserSerializer
|
||||
permission_classes = [IsOrgAdmin]
|
||||
extra_filter_backends = [AssetRelatedByNodeFilterBackend]
|
||||
|
||||
filterset_fields = ['asset', 'username', 'present', 'asset__ip', 'asset__hostname', 'asset_id']
|
||||
|
||||
@@ -17,7 +17,6 @@ from django.db.models import Count
|
||||
|
||||
from common.utils import get_logger
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from ..hands import IsOrgAdmin
|
||||
from ..models import Label
|
||||
from .. import serializers
|
||||
|
||||
@@ -30,7 +29,6 @@ class LabelViewSet(OrgBulkModelViewSet):
|
||||
model = Label
|
||||
filterset_fields = ("name", "value")
|
||||
search_fields = filterset_fields
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.LabelSerializer
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
|
||||
@@ -20,7 +20,6 @@ from common.tree import TreeNodeSerializer
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from orgs.mixins import generics
|
||||
from orgs.utils import current_org
|
||||
from ..hands import IsOrgAdmin
|
||||
from ..models import Node
|
||||
from ..tasks import (
|
||||
update_node_assets_hardware_info_manual,
|
||||
@@ -31,7 +30,6 @@ from .. import serializers
|
||||
from .mixin import SerializeToTreeNodeMixin
|
||||
from assets.locks import NodeAddChildrenLock
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
__all__ = [
|
||||
'NodeViewSet', 'NodeChildrenApi', 'NodeAssetsApi',
|
||||
@@ -45,9 +43,12 @@ __all__ = [
|
||||
class NodeViewSet(SuggestionMixin, OrgBulkModelViewSet):
|
||||
model = Node
|
||||
filterset_fields = ('value', 'key', 'id')
|
||||
search_fields = ('value', )
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
search_fields = ('value',)
|
||||
serializer_class = serializers.NodeSerializer
|
||||
rbac_perms = {
|
||||
'match': 'assets.match_node',
|
||||
'check_assets_amount_task': 'assets.change_node'
|
||||
}
|
||||
|
||||
@action(methods=[POST], detail=False, url_path='check_assets_amount_task')
|
||||
def check_assets_amount_task(self, request):
|
||||
@@ -85,7 +86,6 @@ class NodeListAsTreeApi(generics.ListAPIView):
|
||||
]
|
||||
"""
|
||||
model = Node
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = TreeNodeSerializer
|
||||
|
||||
@staticmethod
|
||||
@@ -100,7 +100,6 @@ class NodeListAsTreeApi(generics.ListAPIView):
|
||||
|
||||
|
||||
class NodeChildrenApi(generics.ListCreateAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.NodeSerializer
|
||||
instance = None
|
||||
is_initial = False
|
||||
@@ -199,7 +198,6 @@ class NodeChildrenAsTreeApi(SerializeToTreeNodeMixin, NodeChildrenApi):
|
||||
|
||||
|
||||
class NodeAssetsApi(generics.ListAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.AssetSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
@@ -214,7 +212,6 @@ class NodeAssetsApi(generics.ListAPIView):
|
||||
|
||||
class NodeAddChildrenApi(generics.UpdateAPIView):
|
||||
model = Node
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.NodeAddChildrenSerializer
|
||||
instance = None
|
||||
|
||||
@@ -231,7 +228,6 @@ class NodeAddChildrenApi(generics.UpdateAPIView):
|
||||
class NodeAddAssetsApi(generics.UpdateAPIView):
|
||||
model = Node
|
||||
serializer_class = serializers.NodeAssetsSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
instance = None
|
||||
|
||||
def perform_update(self, serializer):
|
||||
@@ -243,7 +239,6 @@ class NodeAddAssetsApi(generics.UpdateAPIView):
|
||||
class NodeRemoveAssetsApi(generics.UpdateAPIView):
|
||||
model = Node
|
||||
serializer_class = serializers.NodeAssetsSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
instance = None
|
||||
|
||||
def perform_update(self, serializer):
|
||||
@@ -262,7 +257,6 @@ class NodeRemoveAssetsApi(generics.UpdateAPIView):
|
||||
class MoveAssetsToNodeApi(generics.UpdateAPIView):
|
||||
model = Node
|
||||
serializer_class = serializers.NodeAssetsSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
instance = None
|
||||
|
||||
def perform_update(self, serializer):
|
||||
@@ -303,9 +297,21 @@ class MoveAssetsToNodeApi(generics.UpdateAPIView):
|
||||
|
||||
|
||||
class NodeTaskCreateApi(generics.CreateAPIView):
|
||||
perm_model = Asset
|
||||
model = Node
|
||||
serializer_class = serializers.NodeTaskSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
def check_permissions(self, request):
|
||||
action = request.data.get('action')
|
||||
action_perm_require = {
|
||||
'refresh': 'assets.refresh_assethardwareinfo',
|
||||
'test': 'assets.test_assetconnectivity'
|
||||
}
|
||||
perm_required = action_perm_require.get(action)
|
||||
has = self.request.user.has_perm(perm_required)
|
||||
|
||||
if not has:
|
||||
self.permission_denied(request)
|
||||
|
||||
def get_object(self):
|
||||
node_id = self.kwargs.get('pk')
|
||||
@@ -338,4 +344,3 @@ class NodeTaskCreateApi(generics.CreateAPIView):
|
||||
else:
|
||||
task = test_node_assets_connectivity_manual.delay(node)
|
||||
self.set_serializer_data(serializer, task)
|
||||
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
# ~*~ coding: utf-8 ~*~
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.response import Response
|
||||
from django.db.models import Q
|
||||
from rest_framework.decorators import action
|
||||
|
||||
from common.utils import get_logger, get_object_or_none
|
||||
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsValidUser
|
||||
from common.utils.crypto import get_aes_crypto
|
||||
from common.permissions import IsValidUser
|
||||
from common.mixins.api import SuggestionMixin
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from orgs.mixins import generics
|
||||
from common.mixins.api import SuggestionMixin
|
||||
from orgs.utils import tmp_to_root_org
|
||||
from rest_framework.decorators import action
|
||||
from users.models import User, UserGroup
|
||||
from applications.models import Application
|
||||
from ..models import SystemUser, Asset, CommandFilter, CommandFilterRule
|
||||
from ..models import SystemUser, CommandFilterRule
|
||||
from .. import serializers
|
||||
from ..serializers import SystemUserWithAuthInfoSerializer, SystemUserTempAuthSerializer
|
||||
from ..tasks import (
|
||||
@@ -47,7 +45,11 @@ class SystemUserViewSet(SuggestionMixin, OrgBulkModelViewSet):
|
||||
}
|
||||
ordering_fields = ('name', 'protocol', 'login_mode')
|
||||
ordering = ('name', )
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
rbac_perms = {
|
||||
'su_from': 'assets.view_systemuser',
|
||||
'su_to': 'assets.view_systemuser',
|
||||
'match': 'assets.match_systemuser'
|
||||
}
|
||||
|
||||
@action(methods=['get'], detail=False, url_path='su-from')
|
||||
def su_from(self, request, *args, **kwargs):
|
||||
@@ -81,8 +83,13 @@ class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView):
|
||||
Get system user auth info
|
||||
"""
|
||||
model = SystemUser
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = SystemUserWithAuthInfoSerializer
|
||||
rbac_perms = {
|
||||
'retrieve': 'assets.view_systemusersecret',
|
||||
'list': 'assets.view_systemusersecret',
|
||||
'change': 'assets.change_systemuser',
|
||||
'destroy': 'assets.change_systemuser',
|
||||
}
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
@@ -95,17 +102,27 @@ class SystemUserTempAuthInfoApi(generics.CreateAPIView):
|
||||
permission_classes = (IsValidUser,)
|
||||
serializer_class = SystemUserTempAuthSerializer
|
||||
|
||||
def decrypt_data_if_need(self, data):
|
||||
csrf_token = self.request.META.get('CSRF_COOKIE')
|
||||
aes = get_aes_crypto(csrf_token, 'ECB')
|
||||
password = data.get('password', '')
|
||||
try:
|
||||
data['password'] = aes.decrypt(password)
|
||||
except:
|
||||
pass
|
||||
return data
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = super().get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
pk = kwargs.get('pk')
|
||||
user = self.request.user
|
||||
data = serializer.validated_data
|
||||
data = self.decrypt_data_if_need(serializer.validated_data)
|
||||
instance_id = data.get('instance_id')
|
||||
|
||||
with tmp_to_root_org():
|
||||
instance = get_object_or_404(SystemUser, pk=pk)
|
||||
instance.set_temp_auth(instance_id, user.id, data)
|
||||
instance.set_temp_auth(instance_id, self.request.user.id, data)
|
||||
return Response(serializer.data, status=201)
|
||||
|
||||
|
||||
@@ -114,7 +131,6 @@ class SystemUserAssetAuthInfoApi(generics.RetrieveAPIView):
|
||||
Get system user with asset auth info
|
||||
"""
|
||||
model = SystemUser
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = SystemUserWithAuthInfoSerializer
|
||||
|
||||
def get_object(self):
|
||||
@@ -131,8 +147,10 @@ class SystemUserAppAuthInfoApi(generics.RetrieveAPIView):
|
||||
Get system user with asset auth info
|
||||
"""
|
||||
model = SystemUser
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = SystemUserWithAuthInfoSerializer
|
||||
rbac_perms = {
|
||||
'retrieve': 'assets.view_systemusersecret',
|
||||
}
|
||||
|
||||
def get_object(self):
|
||||
instance = super().get_object()
|
||||
@@ -144,7 +162,6 @@ class SystemUserAppAuthInfoApi(generics.RetrieveAPIView):
|
||||
|
||||
|
||||
class SystemUserTaskApi(generics.CreateAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.SystemUserTaskSerializer
|
||||
|
||||
def do_push(self, system_user, asset_ids=None):
|
||||
@@ -166,6 +183,18 @@ class SystemUserTaskApi(generics.CreateAPIView):
|
||||
pk = self.kwargs.get('pk')
|
||||
return get_object_or_404(SystemUser, pk=pk)
|
||||
|
||||
def check_permissions(self, request):
|
||||
action = request.data.get('action')
|
||||
action_perm_require = {
|
||||
'push': 'assets.push_assetsystemuser',
|
||||
'test': 'assets.test_assetconnectivity'
|
||||
}
|
||||
perm_required = action_perm_require.get(action)
|
||||
has = self.request.user.has_perm(perm_required)
|
||||
|
||||
if not has:
|
||||
self.permission_denied(request)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
action = serializer.validated_data["action"]
|
||||
asset = serializer.validated_data.get('asset')
|
||||
@@ -189,7 +218,9 @@ class SystemUserTaskApi(generics.CreateAPIView):
|
||||
|
||||
|
||||
class SystemUserCommandFilterRuleListApi(generics.ListAPIView):
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
rbac_perms = {
|
||||
'list': 'assets.view_commandfilterule'
|
||||
}
|
||||
|
||||
def get_serializer_class(self):
|
||||
from ..serializers import CommandFilterRuleSerializer
|
||||
@@ -215,10 +246,12 @@ class SystemUserCommandFilterRuleListApi(generics.ListAPIView):
|
||||
|
||||
|
||||
class SystemUserAssetsListView(generics.ListAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.AssetSimpleSerializer
|
||||
filterset_fields = ("hostname", "ip")
|
||||
search_fields = filterset_fields
|
||||
rbac_perms = {
|
||||
'list': 'assets.view_asset'
|
||||
}
|
||||
|
||||
def get_object(self):
|
||||
pk = self.kwargs.get('pk')
|
||||
|
||||
@@ -5,7 +5,6 @@ from django.db.models import F, Value, Model
|
||||
from django.db.models.signals import m2m_changed
|
||||
from django.db.models.functions import Concat
|
||||
|
||||
from common.permissions import IsOrgAdmin
|
||||
from common.utils import get_logger
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from orgs.utils import current_org
|
||||
@@ -65,13 +64,13 @@ class RelationMixin:
|
||||
|
||||
|
||||
class BaseRelationViewSet(RelationMixin, OrgBulkModelViewSet):
|
||||
pass
|
||||
perm_model = models.SystemUser
|
||||
|
||||
|
||||
class SystemUserAssetRelationViewSet(BaseRelationViewSet):
|
||||
perm_model = models.AuthBook
|
||||
serializer_class = serializers.SystemUserAssetRelationSerializer
|
||||
model = models.SystemUser.assets.through
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
filterset_fields = [
|
||||
'id', 'asset', 'systemuser',
|
||||
]
|
||||
@@ -97,7 +96,6 @@ class SystemUserAssetRelationViewSet(BaseRelationViewSet):
|
||||
class SystemUserNodeRelationViewSet(BaseRelationViewSet):
|
||||
serializer_class = serializers.SystemUserNodeRelationSerializer
|
||||
model = models.SystemUser.nodes.through
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
filterset_fields = [
|
||||
'id', 'node', 'systemuser',
|
||||
]
|
||||
@@ -118,7 +116,6 @@ class SystemUserNodeRelationViewSet(BaseRelationViewSet):
|
||||
class SystemUserUserRelationViewSet(BaseRelationViewSet):
|
||||
serializer_class = serializers.SystemUserUserRelationSerializer
|
||||
model = models.SystemUser.users.through
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
filterset_fields = [
|
||||
'id', 'user', 'systemuser',
|
||||
]
|
||||
@@ -140,4 +137,3 @@ class SystemUserUserRelationViewSet(BaseRelationViewSet):
|
||||
)
|
||||
)
|
||||
return queryset
|
||||
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
from __future__ import unicode_literals
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AssetsConfig(AppConfig):
|
||||
name = 'assets'
|
||||
verbose_name = _('App assets')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def ready(self):
|
||||
super().ready()
|
||||
from . import signals_handler
|
||||
from . import signal_handlers
|
||||
|
||||
@@ -11,5 +11,4 @@
|
||||
"""
|
||||
|
||||
|
||||
from common.permissions import IsAppUser, IsOrgAdmin, IsValidUser, IsOrgAdminOrAppUser
|
||||
from users.models import User, UserGroup
|
||||
|
||||
18
apps/assets/migrations/0085_commandfilterrule_ignore_case.py
Normal file
18
apps/assets/migrations/0085_commandfilterrule_ignore_case.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.13 on 2022-02-08 02:57
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0084_auto_20220112_1959'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='commandfilterrule',
|
||||
name='ignore_case',
|
||||
field=models.BooleanField(default=True, verbose_name='Ignore case'),
|
||||
),
|
||||
]
|
||||
25
apps/assets/migrations/0086_auto_20220217_2135.py
Normal file
25
apps/assets/migrations/0086_auto_20220217_2135.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 3.1.13 on 2022-02-17 13:35
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0085_commandfilterrule_ignore_case'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='asset',
|
||||
options={'ordering': ['hostname'], 'permissions': [('test_assetconnectivity', 'Can test asset connectivity'), ('push_assetsystemuser', 'Can push system user to asset')], 'verbose_name': 'Asset'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='authbook',
|
||||
options={'permissions': [('view_assetaccountsecret', 'Can view asset account secret'), ('change_assetaccountsecret', 'Can change asset account secret')], 'verbose_name': 'AuthBook'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='label',
|
||||
options={'verbose_name': 'Label'},
|
||||
),
|
||||
]
|
||||
18
apps/assets/migrations/0087_auto_20220223_1539.py
Normal file
18
apps/assets/migrations/0087_auto_20220223_1539.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.13 on 2022-02-23 07:39
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0086_auto_20220217_2135'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='protocol',
|
||||
field=models.CharField(choices=[('ssh', 'SSH'), ('rdp', 'RDP'), ('telnet', 'Telnet'), ('vnc', 'VNC'), ('mysql', 'MySQL'), ('oracle', 'Oracle'), ('mariadb', 'MariaDB'), ('postgresql', 'PostgreSQL'), ('sqlserver', 'SQLServer'), ('redis', 'Redis'), ('mongodb', 'MongoDB'), ('k8s', 'K8S')], default='ssh', max_length=16, verbose_name='Protocol'),
|
||||
),
|
||||
]
|
||||
25
apps/assets/migrations/0088_auto_20220303_1612.py
Normal file
25
apps/assets/migrations/0088_auto_20220303_1612.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 3.1.14 on 2022-03-03 08:12
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0087_auto_20220223_1539'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='asset',
|
||||
options={'ordering': ['hostname'], 'permissions': [('refresh_assethardwareinfo', 'Can refresh asset hardware info'), ('test_assetconnectivity', 'Can test asset connectivity'), ('push_assetsystemuser', 'Can push system user to asset'), ('match_asset', 'Can match asset')], 'verbose_name': 'Asset'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='node',
|
||||
options={'ordering': ['parent_key', 'value'], 'permissions': [('match_node', 'Can match node')], 'verbose_name': 'Node'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='systemuser',
|
||||
options={'ordering': ['name'], 'permissions': [('match_systemuser', 'Can match system user')], 'verbose_name': 'System user'},
|
||||
),
|
||||
]
|
||||
29
apps/assets/migrations/0089_auto_20220310_0616.py
Normal file
29
apps/assets/migrations/0089_auto_20220310_0616.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# Generated by Django 3.1.14 on 2022-03-09 22:16
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0088_auto_20220303_1612'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='authbook',
|
||||
options={'permissions': [('test_authbook', 'Can test asset account connectivity'), ('view_assetaccountsecret', 'Can view asset account secret'), ('change_assetaccountsecret', 'Can change asset account secret')], 'verbose_name': 'AuthBook'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='systemuser',
|
||||
options={'ordering': ['name'], 'permissions': [('match_systemuser', 'Can match system user')], 'verbose_name': 'System user'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='asset',
|
||||
options={'ordering': ['hostname'], 'permissions': [('refresh_assethardwareinfo', 'Can refresh asset hardware info'), ('test_assetconnectivity', 'Can test asset connectivity'), ('push_assetsystemuser', 'Can push system user to asset'), ('match_asset', 'Can match asset'), ('add_assettonode', 'Add asset to node'), ('move_assettonode', 'Move asset to node')], 'verbose_name': 'Asset'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='gateway',
|
||||
options={'permissions': [('test_gateway', 'Test gateway')], 'verbose_name': 'Gateway'},
|
||||
),
|
||||
]
|
||||
@@ -8,7 +8,6 @@ from functools import reduce
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.db import models
|
||||
from common.db.models import TextChoices
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
@@ -59,7 +58,7 @@ class AssetQuerySet(models.QuerySet):
|
||||
class ProtocolsMixin:
|
||||
protocols = ''
|
||||
|
||||
class Protocol(TextChoices):
|
||||
class Protocol(models.TextChoices):
|
||||
ssh = 'ssh', 'SSH'
|
||||
rdp = 'rdp', 'RDP'
|
||||
telnet = 'telnet', 'Telnet'
|
||||
@@ -355,3 +354,11 @@ class Asset(AbsConnectivity, AbsHardwareInfo, ProtocolsMixin, NodesRelationMixin
|
||||
unique_together = [('org_id', 'hostname')]
|
||||
verbose_name = _("Asset")
|
||||
ordering = ["hostname", ]
|
||||
permissions = [
|
||||
('refresh_assethardwareinfo', _('Can refresh asset hardware info')),
|
||||
('test_assetconnectivity', _('Can test asset connectivity')),
|
||||
('push_assetsystemuser', _('Can push system user to asset')),
|
||||
('match_asset', _('Can match asset')),
|
||||
('add_assettonode', _('Add asset to node')),
|
||||
('move_assettonode', _('Move asset to node')),
|
||||
]
|
||||
|
||||
@@ -26,6 +26,11 @@ class AuthBook(BaseUser, AbsConnectivity):
|
||||
class Meta:
|
||||
verbose_name = _('AuthBook')
|
||||
unique_together = [('username', 'asset', 'systemuser')]
|
||||
permissions = [
|
||||
('test_authbook', _('Can test asset account connectivity')),
|
||||
('view_assetaccountsecret', _('Can view asset account secret')),
|
||||
('change_assetaccountsecret', _('Can change asset account secret'))
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -85,6 +85,7 @@ class CommandFilterRule(OrgModelMixin):
|
||||
validators=[MinValueValidator(1), MaxValueValidator(100)]
|
||||
)
|
||||
content = models.TextField(verbose_name=_("Content"), help_text=_("One line one command"))
|
||||
ignore_case = models.BooleanField(default=True, verbose_name=_('Ignore case'))
|
||||
action = models.IntegerField(default=ActionChoices.deny, choices=ActionChoices.choices, verbose_name=_("Action"))
|
||||
# 动作: 附加字段
|
||||
# - confirm: 命令复核人
|
||||
@@ -129,13 +130,16 @@ class CommandFilterRule(OrgModelMixin):
|
||||
regex.append(r'\b{0}\b'.format(cmd))
|
||||
else:
|
||||
regex.append(r'\b{0}'.format(cmd))
|
||||
s = r'(?i){}'.format('|'.join(regex))
|
||||
s = r'{}'.format('|'.join(regex))
|
||||
return s
|
||||
|
||||
@staticmethod
|
||||
def compile_regex(regex):
|
||||
def compile_regex(regex, ignore_case):
|
||||
try:
|
||||
pattern = re.compile(regex)
|
||||
if ignore_case:
|
||||
pattern = re.compile(regex, re.IGNORECASE)
|
||||
else:
|
||||
pattern = re.compile(regex)
|
||||
except Exception as e:
|
||||
error = _('The generated regular expression is incorrect: {}').format(str(e))
|
||||
logger.error(error)
|
||||
@@ -143,7 +147,7 @@ class CommandFilterRule(OrgModelMixin):
|
||||
return True, '', pattern
|
||||
|
||||
def match(self, data):
|
||||
succeed, error, pattern = self.compile_regex(regex=self.pattern)
|
||||
succeed, error, pattern = self.compile_regex(self.pattern, self.ignore_case)
|
||||
if not succeed:
|
||||
return self.ACTION_UNKNOWN, ''
|
||||
|
||||
@@ -182,13 +186,15 @@ class CommandFilterRule(OrgModelMixin):
|
||||
return ticket
|
||||
|
||||
@classmethod
|
||||
def get_queryset(cls, user_id=None, user_group_id=None, system_user_id=None, asset_id=None, application_id=None):
|
||||
def get_queryset(cls, user_id=None, user_group_id=None, system_user_id=None,
|
||||
asset_id=None, application_id=None, org_id=None):
|
||||
user_groups = []
|
||||
user = get_object_or_none(User, pk=user_id)
|
||||
if user:
|
||||
user_groups.extend(list(user.groups.all()))
|
||||
user_group = get_object_or_none(UserGroup, pk=user_group_id)
|
||||
if user_group:
|
||||
org_id = user_group.org_id
|
||||
user_groups.append(user_group)
|
||||
system_user = get_object_or_none(SystemUser, pk=system_user_id)
|
||||
asset = get_object_or_none(Asset, pk=asset_id)
|
||||
@@ -199,13 +205,18 @@ class CommandFilterRule(OrgModelMixin):
|
||||
if user_groups:
|
||||
q |= Q(user_groups__in=set(user_groups))
|
||||
if system_user:
|
||||
org_id = system_user.org_id
|
||||
q |= Q(system_users=system_user)
|
||||
if asset:
|
||||
org_id = asset.org_id
|
||||
q |= Q(assets=asset)
|
||||
if application:
|
||||
org_id = application.org_id
|
||||
q |= Q(applications=application)
|
||||
if q:
|
||||
cmd_filters = CommandFilter.objects.filter(q).filter(is_active=True)
|
||||
if org_id:
|
||||
cmd_filters = cmd_filters.filter(org_id=org_id)
|
||||
rule_ids = cmd_filters.values_list('rules', flat=True)
|
||||
rules = cls.objects.filter(id__in=rule_ids)
|
||||
else:
|
||||
|
||||
@@ -7,7 +7,6 @@ import random
|
||||
from django.core.cache import cache
|
||||
import paramiko
|
||||
from django.db import models
|
||||
from django.db.models import TextChoices
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.utils import get_logger
|
||||
@@ -55,7 +54,7 @@ class Gateway(BaseUser):
|
||||
UNCONNECTIVE_SILENCE_PERIOD_KEY_TMPL = 'asset_unconnective_gateway_silence_period_{}'
|
||||
UNCONNECTIVE_SILENCE_PERIOD_BEGIN_VALUE = 60 * 5
|
||||
|
||||
class Protocol(TextChoices):
|
||||
class Protocol(models.TextChoices):
|
||||
ssh = 'ssh', 'SSH'
|
||||
|
||||
ip = models.CharField(max_length=128, verbose_name=_('IP'), db_index=True)
|
||||
@@ -71,6 +70,9 @@ class Gateway(BaseUser):
|
||||
class Meta:
|
||||
unique_together = [('name', 'org_id')]
|
||||
verbose_name = _("Gateway")
|
||||
permissions = [
|
||||
('test_gateway', _('Test gateway'))
|
||||
]
|
||||
|
||||
def set_unconnective(self):
|
||||
unconnective_key = self.UNCONNECTIVE_KEY_TMPL.format(self.id)
|
||||
|
||||
@@ -37,3 +37,4 @@ class Label(OrgModelMixin):
|
||||
class Meta:
|
||||
db_table = "assets_label"
|
||||
unique_together = [('name', 'value', 'org_id')]
|
||||
verbose_name = _('Label')
|
||||
|
||||
@@ -558,6 +558,9 @@ class Node(OrgModelMixin, SomeNodesMixin, FamilyMixin, NodeAssetsMixin):
|
||||
class Meta:
|
||||
verbose_name = _("Node")
|
||||
ordering = ['parent_key', 'value']
|
||||
permissions = [
|
||||
('match_node', _('Can match node')),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.full_value
|
||||
|
||||
@@ -5,13 +5,11 @@
|
||||
import logging
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
from django.core.cache import cache
|
||||
|
||||
from common.utils import signer, get_object_or_none
|
||||
from common.db.models import TextChoices
|
||||
from .base import BaseUser
|
||||
from .asset import Asset
|
||||
from .authbook import AuthBook
|
||||
@@ -24,17 +22,18 @@ logger = logging.getLogger(__name__)
|
||||
class ProtocolMixin:
|
||||
protocol: str
|
||||
|
||||
class Protocol(TextChoices):
|
||||
class Protocol(models.TextChoices):
|
||||
ssh = 'ssh', 'SSH'
|
||||
rdp = 'rdp', 'RDP'
|
||||
telnet = 'telnet', 'Telnet'
|
||||
vnc = 'vnc', 'VNC'
|
||||
mysql = 'mysql', 'MySQL'
|
||||
redis = 'redis', 'Redis'
|
||||
oracle = 'oracle', 'Oracle'
|
||||
mariadb = 'mariadb', 'MariaDB'
|
||||
postgresql = 'postgresql', 'PostgreSQL'
|
||||
sqlserver = 'sqlserver', 'SQLServer'
|
||||
redis = 'redis', 'Redis'
|
||||
mongodb = 'mongodb', 'MongoDB'
|
||||
k8s = 'k8s', 'K8S'
|
||||
|
||||
SUPPORT_PUSH_PROTOCOLS = [Protocol.ssh, Protocol.rdp]
|
||||
@@ -46,8 +45,9 @@ class ProtocolMixin:
|
||||
Protocol.rdp
|
||||
]
|
||||
APPLICATION_CATEGORY_DB_PROTOCOLS = [
|
||||
Protocol.mysql, Protocol.redis, Protocol.oracle,
|
||||
Protocol.mariadb, Protocol.postgresql, Protocol.sqlserver
|
||||
Protocol.mysql, Protocol.mariadb, Protocol.oracle,
|
||||
Protocol.postgresql, Protocol.sqlserver,
|
||||
Protocol.redis, Protocol.mongodb
|
||||
]
|
||||
APPLICATION_CATEGORY_CLOUD_PROTOCOLS = [
|
||||
Protocol.k8s
|
||||
@@ -133,6 +133,14 @@ class AuthMixin:
|
||||
self.password = password
|
||||
|
||||
def load_app_more_auth(self, app_id=None, username=None, user_id=None):
|
||||
from applications.models import Application
|
||||
app = get_object_or_none(Application, pk=app_id)
|
||||
if app and app.category_remote_app:
|
||||
# Remote app
|
||||
self._load_remoteapp_more_auth(app, username, user_id)
|
||||
return
|
||||
|
||||
# Other app
|
||||
self._clean_auth_info_if_manual_login_mode()
|
||||
# 加载临时认证信息
|
||||
if self.login_mode == self.LOGIN_MANUAL:
|
||||
@@ -148,19 +156,44 @@ class AuthMixin:
|
||||
_username = username
|
||||
self.username = _username
|
||||
|
||||
def _load_remoteapp_more_auth(self, app, username, user_id):
|
||||
asset = app.get_remote_app_asset(raise_exception=False)
|
||||
if asset:
|
||||
self.load_asset_more_auth(asset_id=asset.id, username=username, user_id=user_id)
|
||||
|
||||
def load_asset_special_auth(self, asset, username=''):
|
||||
"""
|
||||
AuthBook 的数据状态
|
||||
| asset | systemuser | username |
|
||||
1 | * | * | x |
|
||||
2 | * | x | * |
|
||||
|
||||
当前 AuthBook 只有以上两种状态,systemuser 与 username 不会并存。
|
||||
正常的资产与系统用户关联产生的是第1种状态,改密则产生第2种状态。改密之后
|
||||
只有 username 而没有 systemuser 。
|
||||
|
||||
Freq: 关联同一资产的多个系统用户指定同一用户名时,修改用户密码会影响所有系统用户
|
||||
|
||||
这里有一个不对称的行为,同名系统用户密码覆盖
|
||||
当有相同 username 的多个系统用户时,有改密动作之后,所有的同名系统用户都使用最后
|
||||
一次改动,但如果没有发生过改密,同名系统用户使用的密码还是各自的。
|
||||
|
||||
"""
|
||||
authbooks = list(AuthBook.objects.filter(asset=asset).filter(
|
||||
Q(username=username) | Q(systemuser=self)
|
||||
))
|
||||
if len(authbooks) == 0:
|
||||
if username == '':
|
||||
username = self.username
|
||||
|
||||
authbook = AuthBook.objects.filter(
|
||||
asset=asset, username=username, systemuser__isnull=True
|
||||
).order_by('-date_created').first()
|
||||
|
||||
if not authbook:
|
||||
authbook = AuthBook.objects.filter(
|
||||
asset=asset, systemuser=self
|
||||
).order_by('-date_created').first()
|
||||
|
||||
if not authbook:
|
||||
return None
|
||||
elif len(authbooks) == 1:
|
||||
authbook = authbooks[0]
|
||||
else:
|
||||
authbooks.sort(key=lambda x: 1 if x.username == username else 0, reverse=True)
|
||||
authbook = authbooks[0]
|
||||
|
||||
authbook.load_auth()
|
||||
self.password = authbook.password
|
||||
self.private_key = authbook.private_key
|
||||
@@ -197,7 +230,7 @@ class SystemUser(ProtocolMixin, AuthMixin, BaseUser):
|
||||
(LOGIN_MANUAL, _('Manually input'))
|
||||
)
|
||||
|
||||
class Type(TextChoices):
|
||||
class Type(models.TextChoices):
|
||||
common = 'common', _('Common user')
|
||||
admin = 'admin', _('Admin user')
|
||||
|
||||
@@ -303,9 +336,12 @@ class SystemUser(ProtocolMixin, AuthMixin, BaseUser):
|
||||
ordering = ['name']
|
||||
unique_together = [('name', 'org_id')]
|
||||
verbose_name = _("System user")
|
||||
permissions = [
|
||||
('match_systemuser', _('Can match system user')),
|
||||
]
|
||||
|
||||
|
||||
# Todo: 准备废弃
|
||||
# Deprecated: 准备废弃
|
||||
class AdminUser(BaseUser):
|
||||
"""
|
||||
A privileged user that ansible can use it to push system user and so on
|
||||
|
||||
@@ -6,6 +6,7 @@ from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
|
||||
from .base import AuthSerializerMixin
|
||||
from .utils import validate_password_contains_left_double_curly_bracket
|
||||
from common.utils.encode import ssh_pubkey_gen
|
||||
|
||||
|
||||
class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
@@ -40,6 +41,21 @@ class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
}
|
||||
ref_name = 'AssetAccountSerializer'
|
||||
|
||||
def _validate_gen_key(self, attrs):
|
||||
private_key = attrs.get('private_key')
|
||||
if not private_key:
|
||||
return attrs
|
||||
|
||||
password = attrs.get('passphrase')
|
||||
username = attrs.get('username')
|
||||
public_key = ssh_pubkey_gen(private_key, password=password, username=username)
|
||||
attrs['public_key'] = public_key
|
||||
return attrs
|
||||
|
||||
def validate(self, attrs):
|
||||
attrs = self._validate_gen_key(attrs)
|
||||
return attrs
|
||||
|
||||
def get_protocols(self, v):
|
||||
return v.protocols.replace(' ', ', ')
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
from io import StringIO
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.utils import ssh_pubkey_gen, ssh_private_key_gen, validate_ssh_private_key
|
||||
|
||||
@@ -12,13 +12,11 @@ from terminal.models import Session
|
||||
|
||||
|
||||
class CommandFilterSerializer(BulkOrgResourceModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = CommandFilter
|
||||
fields_mini = ['id', 'name']
|
||||
fields_small = fields_mini + [
|
||||
'org_id', 'org_name',
|
||||
'is_active',
|
||||
'org_id', 'org_name', 'is_active',
|
||||
'date_created', 'date_updated',
|
||||
'comment', 'created_by',
|
||||
]
|
||||
@@ -26,7 +24,9 @@ class CommandFilterSerializer(BulkOrgResourceModelSerializer):
|
||||
fields_m2m = ['users', 'user_groups', 'system_users', 'assets', 'applications']
|
||||
fields = fields_small + fields_fk + fields_m2m
|
||||
extra_kwargs = {
|
||||
'rules': {'read_only': True}
|
||||
'rules': {'read_only': True},
|
||||
'date_created': {'label': _("Date created")},
|
||||
'date_updated': {'label': _("Date updated")},
|
||||
}
|
||||
|
||||
|
||||
@@ -38,13 +38,18 @@ class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer):
|
||||
model = CommandFilterRule
|
||||
fields_mini = ['id']
|
||||
fields_small = fields_mini + [
|
||||
'type', 'type_display', 'content', 'pattern', 'priority',
|
||||
'type', 'type_display', 'content', 'ignore_case', 'pattern', 'priority',
|
||||
'action', 'action_display', 'reviewers',
|
||||
'date_created', 'date_updated',
|
||||
'comment', 'created_by',
|
||||
]
|
||||
fields_fk = ['filter']
|
||||
fields = fields_small + fields_fk
|
||||
extra_kwargs = {
|
||||
'date_created': {'label': _("Date created")},
|
||||
'date_updated': {'label': _("Date updated")},
|
||||
'action_display': {'label': _("Action display")}
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -66,7 +71,8 @@ class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer):
|
||||
regex = CommandFilterRule.construct_command_regex(content)
|
||||
else:
|
||||
regex = content
|
||||
succeed, error, pattern = CommandFilterRule.compile_regex(regex)
|
||||
ignore_case = self.initial_data.get('ignore_case')
|
||||
succeed, error, pattern = CommandFilterRule.compile_regex(regex, ignore_case)
|
||||
if not succeed:
|
||||
raise serializers.ValidationError(error)
|
||||
return content
|
||||
|
||||
@@ -27,7 +27,7 @@ class LabelSerializer(BulkOrgResourceModelSerializer):
|
||||
'category', 'date_created', 'asset_count',
|
||||
)
|
||||
extra_kwargs = {
|
||||
'assets': {'required': False}
|
||||
'assets': {'required': False, 'label': _('Asset')}
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -5,7 +5,6 @@ from django.utils.translation import ugettext as _
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from ..models import Asset, Node
|
||||
|
||||
|
||||
__all__ = [
|
||||
'NodeSerializer', "NodeAddChildrenSerializer",
|
||||
"NodeAssetsSerializer", "NodeTaskSerializer",
|
||||
@@ -45,7 +44,6 @@ class NodeSerializer(BulkOrgResourceModelSerializer):
|
||||
|
||||
def create(self, validated_data):
|
||||
full_value = validated_data.get('full_value')
|
||||
value = validated_data.get('value')
|
||||
|
||||
# 直接多层级创建
|
||||
if full_value:
|
||||
@@ -53,7 +51,8 @@ class NodeSerializer(BulkOrgResourceModelSerializer):
|
||||
# 根据 value 在 root 下创建
|
||||
else:
|
||||
key = Node.org_root().get_next_child_key()
|
||||
node = Node.objects.create(key=key, value=value)
|
||||
validated_data['key'] = key
|
||||
node = Node.objects.create(**validated_data)
|
||||
return node
|
||||
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ def subscribe_node_assets_mapping_expire(sender, **kwargs):
|
||||
Node.expire_node_all_asset_ids_mapping_from_memory(root_org_id)
|
||||
|
||||
def keep_subscribe_node_assets_relation():
|
||||
node_assets_mapping_for_memory_pub_sub.keep_handle_msg(handle_node_relation_change)
|
||||
node_assets_mapping_for_memory_pub_sub.subscribe(handle_node_relation_change)
|
||||
|
||||
t = threading.Thread(target=keep_subscribe_node_assets_relation)
|
||||
t.daemon = True
|
||||
@@ -52,10 +52,11 @@ class BaseAccountHandler:
|
||||
return header_fields
|
||||
|
||||
@classmethod
|
||||
def create_row(cls, account, serializer_cls):
|
||||
def create_row(cls, account, serializer_cls, header_fields=None):
|
||||
serializer = serializer_cls(account)
|
||||
if not header_fields:
|
||||
header_fields = cls.get_header_fields(serializer)
|
||||
data = cls.unpack_data(serializer.data)
|
||||
header_fields = cls.get_header_fields(serializer)
|
||||
row_dict = {}
|
||||
for field, header_name in header_fields.items():
|
||||
row_dict[header_name] = data[field]
|
||||
@@ -74,16 +75,20 @@ class AssetAccountHandler(BaseAccountHandler):
|
||||
def create_df(cls):
|
||||
df_dict = defaultdict(list)
|
||||
sheet_name = AuthBook._meta.verbose_name
|
||||
accounts = AuthBook.get_queryset()
|
||||
|
||||
accounts = AuthBook.get_queryset().select_related('systemuser')
|
||||
if not accounts.first():
|
||||
return df_dict
|
||||
|
||||
header_fields = cls.get_header_fields(AccountSecretSerializer(accounts.first()))
|
||||
for account in accounts:
|
||||
account.load_auth()
|
||||
row = cls.create_row(account, AccountSecretSerializer)
|
||||
row = cls.create_row(account, AccountSecretSerializer, header_fields)
|
||||
df_dict[sheet_name].append(row)
|
||||
|
||||
for k, v in df_dict.items():
|
||||
df_dict[k] = pd.DataFrame(v)
|
||||
|
||||
logger.info('\n\033[33m- 共收集{}条资产账号\033[0m'.format(accounts.count()))
|
||||
logger.info('\n\033[33m- 共收集 {} 条资产账号\033[0m'.format(accounts.count()))
|
||||
return df_dict
|
||||
|
||||
|
||||
@@ -98,7 +103,7 @@ class AppAccountHandler(BaseAccountHandler):
|
||||
@classmethod
|
||||
def create_df(cls):
|
||||
df_dict = defaultdict(list)
|
||||
accounts = Account.get_queryset()
|
||||
accounts = Account.get_queryset().select_related('systemuser')
|
||||
for account in accounts:
|
||||
account.load_auth()
|
||||
app_type = account.type
|
||||
|
||||
@@ -4,10 +4,10 @@ from itertools import groupby
|
||||
from celery import shared_task
|
||||
from common.db.utils import get_object_if_need, get_objects
|
||||
from django.utils.translation import ugettext as _, gettext_noop
|
||||
from django.db.models import Empty, Q
|
||||
from django.db.models import Empty
|
||||
|
||||
from common.utils import encrypt_password, get_logger
|
||||
from assets.models import SystemUser, Asset, AuthBook
|
||||
from assets.models import SystemUser, Asset
|
||||
from orgs.utils import org_aware_func, tmp_to_root_org
|
||||
from . import const
|
||||
from .utils import clean_ansible_task_hosts, group_asset_by_platform
|
||||
@@ -178,6 +178,7 @@ def get_push_windows_system_user_tasks(system_user: SystemUser, username=None):
|
||||
|
||||
def get_push_system_user_tasks(system_user, platform="unixlike", username=None):
|
||||
"""
|
||||
获取推送系统用户的 ansible 命令,跟资产无关
|
||||
:param system_user:
|
||||
:param platform:
|
||||
:param username: 当动态时,近推送某个
|
||||
@@ -209,18 +210,10 @@ def push_system_user_util(system_user, assets, task_name, username=None):
|
||||
if not assets:
|
||||
return {}
|
||||
|
||||
# 资产按平台分类
|
||||
assets_sorted = sorted(assets, key=group_asset_by_platform)
|
||||
platform_hosts = groupby(assets_sorted, key=group_asset_by_platform)
|
||||
|
||||
def run_task(_tasks, _hosts):
|
||||
if not _tasks:
|
||||
return
|
||||
task, created = update_or_create_ansible_task(
|
||||
task_name=task_name, hosts=_hosts, tasks=_tasks, pattern='all',
|
||||
options=const.TASK_OPTIONS, run_as_admin=True,
|
||||
)
|
||||
task.run()
|
||||
|
||||
if system_user.username_same_with_user:
|
||||
if username is None:
|
||||
# 动态系统用户,但是没有指定 username
|
||||
@@ -232,6 +225,15 @@ def push_system_user_util(system_user, assets, task_name, username=None):
|
||||
assert username is None, 'Only Dynamic user can assign `username`'
|
||||
usernames = [system_user.username]
|
||||
|
||||
def run_task(_tasks, _hosts):
|
||||
if not _tasks:
|
||||
return
|
||||
task, created = update_or_create_ansible_task(
|
||||
task_name=task_name, hosts=_hosts, tasks=_tasks, pattern='all',
|
||||
options=const.TASK_OPTIONS, run_as_admin=True,
|
||||
)
|
||||
task.run()
|
||||
|
||||
for platform, _assets in platform_hosts:
|
||||
_assets = list(_assets)
|
||||
if not _assets:
|
||||
@@ -239,36 +241,11 @@ def push_system_user_util(system_user, assets, task_name, username=None):
|
||||
print(_("Start push system user for platform: [{}]").format(platform))
|
||||
print(_("Hosts count: {}").format(len(_assets)))
|
||||
|
||||
id_asset_map = {_asset.id: _asset for _asset in _assets}
|
||||
asset_ids = id_asset_map.keys()
|
||||
no_special_auth = []
|
||||
special_auth_set = set()
|
||||
|
||||
auth_books = AuthBook.objects.filter(asset_id__in=asset_ids).filter(
|
||||
Q(username__in=usernames) | Q(systemuser=system_user)
|
||||
).prefetch_related('systemuser')
|
||||
|
||||
for auth_book in auth_books:
|
||||
auth_book.load_auth()
|
||||
special_auth_set.add((auth_book.username, auth_book.asset_id))
|
||||
|
||||
for _username in usernames:
|
||||
no_special_assets = []
|
||||
for asset_id in asset_ids:
|
||||
if (_username, asset_id) not in special_auth_set:
|
||||
no_special_assets.append(id_asset_map[asset_id])
|
||||
if no_special_assets:
|
||||
no_special_auth.append((_username, no_special_assets))
|
||||
|
||||
for _username, no_special_assets in no_special_auth:
|
||||
tasks = get_push_system_user_tasks(system_user, platform, username=_username)
|
||||
run_task(tasks, no_special_assets)
|
||||
|
||||
for auth_book in auth_books:
|
||||
system_user._merge_auth(auth_book)
|
||||
tasks = get_push_system_user_tasks(system_user, platform, username=auth_book.username)
|
||||
asset = id_asset_map[auth_book.asset_id]
|
||||
run_task(tasks, [asset])
|
||||
for u in usernames:
|
||||
for a in _assets:
|
||||
system_user.load_asset_special_auth(a, u)
|
||||
tasks = get_push_system_user_tasks(system_user, platform, username=u)
|
||||
run_task(tasks, [a])
|
||||
|
||||
|
||||
@shared_task(queue="ansible")
|
||||
|
||||
@@ -26,8 +26,8 @@ router.register(r'favorite-assets', api.FavoriteAssetViewSet, 'favorite-asset')
|
||||
router.register(r'system-users-assets-relations', api.SystemUserAssetRelationViewSet, 'system-users-assets-relation')
|
||||
router.register(r'system-users-nodes-relations', api.SystemUserNodeRelationViewSet, 'system-users-nodes-relation')
|
||||
router.register(r'system-users-users-relations', api.SystemUserUserRelationViewSet, 'system-users-users-relation')
|
||||
router.register(r'backup', api.AccountBackupPlanViewSet, 'backup')
|
||||
router.register(r'backup-execution', api.AccountBackupPlanExecutionViewSet, 'backup-execution')
|
||||
router.register(r'account-backup-plans', api.AccountBackupPlanViewSet, 'account-backup')
|
||||
router.register(r'account-backup-plan-executions', api.AccountBackupPlanExecutionViewSet, 'account-backup-execution')
|
||||
|
||||
cmd_filter_router = routers.NestedDefaultRouter(router, r'cmd-filters', lookup='filter')
|
||||
cmd_filter_router.register(r'rules', api.CommandFilterRuleViewSet, 'cmd-filter-rule')
|
||||
@@ -68,7 +68,6 @@ urlpatterns = [
|
||||
path('gateways/<uuid:pk>/test-connective/', api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'),
|
||||
|
||||
path('cmd-filters/command-confirm/', api.CommandConfirmAPI.as_view(), name='command-confirm'),
|
||||
path('cmd-filters/command-confirm/<uuid:pk>/status/', api.CommandConfirmStatusAPI.as_view(), name='command-confirm-status')
|
||||
|
||||
]
|
||||
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
from rest_framework.mixins import ListModelMixin, CreateModelMixin
|
||||
from django.db.models import F, Value
|
||||
from django.db.models.functions import Concat
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework import generics
|
||||
|
||||
from common.permissions import IsOrgAdminOrAppUser, IsOrgAuditor, IsOrgAdmin
|
||||
from common.drf.api import JMSReadOnlyModelViewSet
|
||||
from common.drf.filters import DatetimeRangeFilter
|
||||
from common.api import CommonGenericViewSet
|
||||
from orgs.mixins.api import OrgGenericViewSet, OrgBulkModelViewSet, OrgRelationMixin
|
||||
@@ -20,7 +22,6 @@ class FTPLogViewSet(CreateModelMixin,
|
||||
OrgGenericViewSet):
|
||||
model = FTPLog
|
||||
serializer_class = FTPLogSerializer
|
||||
permission_classes = (IsOrgAdminOrAppUser | IsOrgAuditor,)
|
||||
extra_filter_backends = [DatetimeRangeFilter]
|
||||
date_range_filter_fields = [
|
||||
('date_start', ('date_from', 'date_to'))
|
||||
@@ -30,9 +31,8 @@ class FTPLogViewSet(CreateModelMixin,
|
||||
ordering = ['-date_start']
|
||||
|
||||
|
||||
class UserLoginLogViewSet(ListModelMixin, CommonGenericViewSet):
|
||||
class UserLoginCommonMixin:
|
||||
queryset = UserLoginLog.objects.all()
|
||||
permission_classes = [IsOrgAdmin | IsOrgAuditor]
|
||||
serializer_class = UserLoginLogSerializer
|
||||
extra_filter_backends = [DatetimeRangeFilter]
|
||||
date_range_filter_fields = [
|
||||
@@ -41,6 +41,9 @@ class UserLoginLogViewSet(ListModelMixin, CommonGenericViewSet):
|
||||
filterset_fields = ['username', 'ip', 'city', 'type', 'status', 'mfa']
|
||||
search_fields = ['username', 'ip', 'city']
|
||||
|
||||
|
||||
class UserLoginLogViewSet(UserLoginCommonMixin, ListModelMixin, CommonGenericViewSet):
|
||||
|
||||
@staticmethod
|
||||
def get_org_members():
|
||||
users = current_org.get_members().values_list('username', flat=True)
|
||||
@@ -55,10 +58,18 @@ class UserLoginLogViewSet(ListModelMixin, CommonGenericViewSet):
|
||||
return queryset
|
||||
|
||||
|
||||
class MyLoginLogAPIView(UserLoginCommonMixin, generics.ListAPIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset()
|
||||
qs = qs.filter(username=self.request.user.username)
|
||||
return qs
|
||||
|
||||
|
||||
class OperateLogViewSet(ListModelMixin, OrgGenericViewSet):
|
||||
model = OperateLog
|
||||
serializer_class = OperateLogSerializer
|
||||
permission_classes = [IsOrgAdmin | IsOrgAuditor]
|
||||
extra_filter_backends = [DatetimeRangeFilter]
|
||||
date_range_filter_fields = [
|
||||
('datetime', ('date_from', 'date_to'))
|
||||
@@ -70,7 +81,6 @@ class OperateLogViewSet(ListModelMixin, OrgGenericViewSet):
|
||||
|
||||
class PasswordChangeLogViewSet(ListModelMixin, CommonGenericViewSet):
|
||||
queryset = PasswordChangeLog.objects.all()
|
||||
permission_classes = [IsOrgAdmin | IsOrgAuditor]
|
||||
serializer_class = PasswordChangeLogSerializer
|
||||
extra_filter_backends = [DatetimeRangeFilter]
|
||||
date_range_filter_fields = [
|
||||
@@ -91,7 +101,6 @@ class PasswordChangeLogViewSet(ListModelMixin, CommonGenericViewSet):
|
||||
class CommandExecutionViewSet(ListModelMixin, OrgGenericViewSet):
|
||||
model = CommandExecution
|
||||
serializer_class = CommandExecutionSerializer
|
||||
permission_classes = [IsOrgAdmin | IsOrgAuditor]
|
||||
extra_filter_backends = [DatetimeRangeFilter]
|
||||
date_range_filter_fields = [
|
||||
('date_start', ('date_from', 'date_to'))
|
||||
@@ -117,12 +126,15 @@ class CommandExecutionViewSet(ListModelMixin, OrgGenericViewSet):
|
||||
class CommandExecutionHostRelationViewSet(OrgRelationMixin, OrgBulkModelViewSet):
|
||||
serializer_class = CommandExecutionHostsRelationSerializer
|
||||
m2m_field = CommandExecution.hosts.field
|
||||
permission_classes = [IsOrgAdmin | IsOrgAuditor]
|
||||
filterset_fields = [
|
||||
'id', 'asset', 'commandexecution'
|
||||
]
|
||||
search_fields = ('asset__hostname', )
|
||||
http_method_names = ['options', 'get']
|
||||
rbac_perms = {
|
||||
'GET': 'ops.view_commandexecution',
|
||||
'list': 'ops.view_commandexecution',
|
||||
}
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
from django.apps import AppConfig
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.db.models.signals import post_save
|
||||
|
||||
|
||||
class AuditsConfig(AppConfig):
|
||||
name = 'audits'
|
||||
verbose_name = _('Audits')
|
||||
|
||||
def ready(self):
|
||||
from . import signals_handler
|
||||
from . import signal_handlers
|
||||
if settings.SYSLOG_ENABLE:
|
||||
post_save.connect(signals_handler.on_audits_log_create)
|
||||
post_save.connect(signal_handlers.on_audits_log_create)
|
||||
|
||||
29
apps/audits/migrations/0013_auto_20211130_1037.py
Normal file
29
apps/audits/migrations/0013_auto_20211130_1037.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# Generated by Django 3.1.13 on 2021-11-30 02:37
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('audits', '0012_auto_20210414_1443'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='ftplog',
|
||||
options={'verbose_name': 'File transfer log'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='operatelog',
|
||||
options={'verbose_name': 'Operate log'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='passwordchangelog',
|
||||
options={'verbose_name': 'Password change log'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='userloginlog',
|
||||
options={'ordering': ['-datetime', 'username'], 'verbose_name': 'User login log'},
|
||||
),
|
||||
]
|
||||
@@ -43,6 +43,9 @@ class FTPLog(OrgModelMixin):
|
||||
is_success = models.BooleanField(default=True, verbose_name=_("Success"))
|
||||
date_start = models.DateTimeField(auto_now_add=True, verbose_name=_('Date start'))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("File transfer log")
|
||||
|
||||
|
||||
class OperateLog(OrgModelMixin):
|
||||
ACTION_CREATE = 'create'
|
||||
@@ -73,6 +76,9 @@ class OperateLog(OrgModelMixin):
|
||||
self.org_id = Organization.ROOT_ID
|
||||
return super(OperateLog, self).save(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Operate log")
|
||||
|
||||
|
||||
class PasswordChangeLog(models.Model):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
@@ -84,6 +90,9 @@ class PasswordChangeLog(models.Model):
|
||||
def __str__(self):
|
||||
return "{} change {}'s password".format(self.change_by, self.user)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Password change log')
|
||||
|
||||
|
||||
class UserLoginLog(models.Model):
|
||||
LOGIN_TYPE_CHOICE = (
|
||||
@@ -155,3 +164,4 @@ class UserLoginLog(models.Model):
|
||||
|
||||
class Meta:
|
||||
ordering = ['-datetime', 'username']
|
||||
verbose_name = _('User login log')
|
||||
|
||||
@@ -22,13 +22,15 @@ from users.models import User
|
||||
from users.signals import post_user_change_password
|
||||
from terminal.models import Session, Command
|
||||
from .utils import write_login_log
|
||||
from . import models
|
||||
from . import models, serializers
|
||||
from .models import OperateLog
|
||||
from orgs.utils import current_org
|
||||
from perms.models import AssetPermission, ApplicationPermission
|
||||
from terminal.backends.command.serializers import SessionCommandSerializer
|
||||
from terminal.serializers import SessionSerializer
|
||||
from common.const.signals import POST_ADD, POST_REMOVE, POST_CLEAR
|
||||
from common.utils import get_request_ip, get_logger, get_syslogger
|
||||
from common.utils.encode import model_to_json
|
||||
from common.utils.encode import data_to_json
|
||||
|
||||
logger = get_logger(__name__)
|
||||
sys_logger = get_syslogger(__name__)
|
||||
@@ -100,11 +102,6 @@ def create_operate_log(action, sender, resource):
|
||||
|
||||
|
||||
M2M_NEED_RECORD = {
|
||||
'OrganizationMember': (
|
||||
_('User and Organization'),
|
||||
_('{User} JOINED {Organization}'),
|
||||
_('{User} LEFT {Organization}')
|
||||
),
|
||||
User.groups.through._meta.object_name: (
|
||||
_('User and Group'),
|
||||
_('{User} JOINED {UserGroup}'),
|
||||
@@ -168,9 +165,9 @@ M2M_NEED_RECORD = {
|
||||
}
|
||||
|
||||
M2M_ACTION = {
|
||||
POST_ADD: 'add',
|
||||
POST_REMOVE: 'remove',
|
||||
POST_CLEAR: 'remove',
|
||||
POST_ADD: OperateLog.ACTION_CREATE,
|
||||
POST_REMOVE: OperateLog.ACTION_DELETE,
|
||||
POST_CLEAR: OperateLog.ACTION_DELETE,
|
||||
}
|
||||
|
||||
|
||||
@@ -185,14 +182,14 @@ def on_m2m_changed(sender, action, instance, reverse, model, pk_set, **kwargs):
|
||||
|
||||
sender_name = sender._meta.object_name
|
||||
if sender_name in M2M_NEED_RECORD:
|
||||
action = M2M_ACTION[action]
|
||||
org_id = current_org.id
|
||||
remote_addr = get_request_ip(current_request)
|
||||
user = str(user)
|
||||
resource_type, resource_tmpl_add, resource_tmpl_remove = M2M_NEED_RECORD[sender_name]
|
||||
if action == 'add':
|
||||
action = M2M_ACTION[action]
|
||||
if action == OperateLog.ACTION_CREATE:
|
||||
resource_tmpl = resource_tmpl_add
|
||||
elif action == 'remove':
|
||||
elif action == OperateLog.ACTION_DELETE:
|
||||
resource_tmpl = resource_tmpl_remove
|
||||
|
||||
to_create = []
|
||||
@@ -255,20 +252,27 @@ def on_user_change_password(sender, user=None, **kwargs):
|
||||
def on_audits_log_create(sender, instance=None, **kwargs):
|
||||
if sender == models.UserLoginLog:
|
||||
category = "login_log"
|
||||
serializer_cls = serializers.UserLoginLogSerializer
|
||||
elif sender == models.FTPLog:
|
||||
category = "ftp_log"
|
||||
serializer_cls = serializers.FTPLogSerializer
|
||||
elif sender == models.OperateLog:
|
||||
category = "operation_log"
|
||||
serializer_cls = serializers.OperateLogSerializer
|
||||
elif sender == models.PasswordChangeLog:
|
||||
category = "password_change_log"
|
||||
serializer_cls = serializers.PasswordChangeLogSerializer
|
||||
elif sender == Session:
|
||||
category = "host_session_log"
|
||||
serializer_cls = SessionSerializer
|
||||
elif sender == Command:
|
||||
category = "session_command_log"
|
||||
serializer_cls = SessionCommandSerializer
|
||||
else:
|
||||
return
|
||||
|
||||
data = model_to_json(instance, indent=None)
|
||||
serializer = serializer_cls(instance)
|
||||
data = data_to_json(serializer.data, indent=None)
|
||||
msg = "{} - {}".format(category, data)
|
||||
sys_logger.info(msg)
|
||||
|
||||
@@ -7,7 +7,7 @@ from celery import shared_task
|
||||
from ops.celery.decorator import (
|
||||
register_as_period_task
|
||||
)
|
||||
from .models import UserLoginLog, OperateLog
|
||||
from .models import UserLoginLog, OperateLog, FTPLog
|
||||
from common.utils import get_log_keep_day
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ def clean_ftp_log_period():
|
||||
now = timezone.now()
|
||||
days = get_log_keep_day('FTP_LOG_KEEP_DAYS')
|
||||
expired_day = now - datetime.timedelta(days=days)
|
||||
OperateLog.objects.filter(datetime__lt=expired_day).delete()
|
||||
FTPLog.objects.filter(datetime__lt=expired_day).delete()
|
||||
|
||||
|
||||
@register_as_period_task(interval=3600*24)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# ~*~ coding: utf-8 ~*~
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.urls.conf import re_path
|
||||
from django.urls.conf import re_path, path
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from common import api as capi
|
||||
@@ -20,6 +20,7 @@ router.register(r'command-executions-hosts-relations', api.CommandExecutionHostR
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('my-login-logs/', api.MyLoginLogAPIView.as_view(), name='my-login-log'),
|
||||
]
|
||||
|
||||
old_version_urlpatterns = [
|
||||
|
||||
@@ -8,7 +8,6 @@ from .. import serializers
|
||||
|
||||
|
||||
class AccessKeyViewSet(ModelViewSet):
|
||||
permission_classes = (IsValidUser,)
|
||||
serializer_class = serializers.AccessKeySerializer
|
||||
search_fields = ['^id', '^secret']
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.http import HttpResponse
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext as _
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.request import Request
|
||||
@@ -20,15 +21,15 @@ from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework import serializers
|
||||
|
||||
from applications.models import Application
|
||||
from authentication.signals import post_auth_failed, post_auth_success
|
||||
from authentication.signals import post_auth_failed
|
||||
from common.utils import get_logger, random_string
|
||||
from common.mixins.api import SerializerMixin
|
||||
from common.permissions import IsSuperUserOrAppUser, IsValidUser, IsSuperUser
|
||||
from common.utils.common import get_file_by_arch
|
||||
from orgs.mixins.api import RootOrgViewMixin
|
||||
from common.http import is_true
|
||||
from perms.utils.asset.permission import get_asset_system_user_ids_with_actions_by_user
|
||||
from perms.models.base import Action
|
||||
from perms.utils.application.permission import get_application_actions
|
||||
from perms.utils.asset.permission import get_asset_actions
|
||||
|
||||
from ..serializers import (
|
||||
ConnectionTokenSerializer, ConnectionTokenSecretSerializer,
|
||||
@@ -39,6 +40,14 @@ __all__ = ['UserConnectionTokenViewSet']
|
||||
|
||||
|
||||
class ClientProtocolMixin:
|
||||
"""
|
||||
下载客户端支持的连接文件,里面包含了 token,和 其他连接信息
|
||||
|
||||
- [x] RDP
|
||||
- [ ] KoKo
|
||||
|
||||
本质上,这里还是暴露出 token 来,进行使用
|
||||
"""
|
||||
request: Request
|
||||
get_serializer: Callable
|
||||
create_token: Callable
|
||||
@@ -96,13 +105,17 @@ class ClientProtocolMixin:
|
||||
width = self.request.query_params.get('width')
|
||||
full_screen = is_true(self.request.query_params.get('full_screen'))
|
||||
drives_redirect = is_true(self.request.query_params.get('drives_redirect'))
|
||||
token = self.create_token(user, asset, application, system_user)
|
||||
token, secret = self.create_token(user, asset, application, system_user)
|
||||
|
||||
# 设置磁盘挂载
|
||||
if drives_redirect and asset:
|
||||
systemuser_actions_mapper = get_asset_system_user_ids_with_actions_by_user(user, asset)
|
||||
actions = systemuser_actions_mapper.get(system_user.id, 0)
|
||||
if actions & Action.UPDOWNLOAD:
|
||||
if drives_redirect:
|
||||
actions = 0
|
||||
if asset:
|
||||
actions = get_asset_actions(user, asset, system_user)
|
||||
elif application:
|
||||
actions = get_application_actions(user, application, system_user)
|
||||
|
||||
if actions & Action.UPDOWNLOAD == Action.UPDOWNLOAD:
|
||||
options['drivestoredirect:s'] = '*'
|
||||
|
||||
# 全屏
|
||||
@@ -148,7 +161,6 @@ class ClientProtocolMixin:
|
||||
return name, content
|
||||
|
||||
def get_encrypt_cmdline(self, app: Application):
|
||||
|
||||
parameters = app.get_rdp_remote_app_setting()['parameters']
|
||||
parameters = parameters.encode('ascii')
|
||||
|
||||
@@ -161,7 +173,7 @@ class ClientProtocolMixin:
|
||||
rst = rst.decode('ascii')
|
||||
return rst
|
||||
|
||||
@action(methods=['POST', 'GET'], detail=False, url_path='rdp/file', permission_classes=[IsValidUser])
|
||||
@action(methods=['POST', 'GET'], detail=False, url_path='rdp/file')
|
||||
def get_rdp_file(self, request, *args, **kwargs):
|
||||
if self.request.method == 'GET':
|
||||
data = self.request.query_params
|
||||
@@ -208,7 +220,7 @@ class ClientProtocolMixin:
|
||||
}
|
||||
return data
|
||||
|
||||
@action(methods=['POST', 'GET'], detail=False, url_path='client-url', permission_classes=[IsValidUser])
|
||||
@action(methods=['POST', 'GET'], detail=False, url_path='client-url')
|
||||
def get_client_protocol_url(self, request, *args, **kwargs):
|
||||
serializer = self.get_valid_serializer()
|
||||
try:
|
||||
@@ -231,19 +243,16 @@ class SecretDetailMixin:
|
||||
|
||||
@staticmethod
|
||||
def _get_application_secret_detail(application):
|
||||
from perms.models.base import Action
|
||||
gateway = None
|
||||
remote_app = None
|
||||
asset = None
|
||||
|
||||
if not application.category_remote_app:
|
||||
actions = Action.NONE
|
||||
remote_app = {}
|
||||
asset = None
|
||||
domain = application.domain
|
||||
else:
|
||||
if application.category_remote_app:
|
||||
remote_app = application.get_rdp_remote_app_setting()
|
||||
actions = Action.CONNECT
|
||||
asset = application.get_remote_app_asset()
|
||||
domain = asset.domain
|
||||
else:
|
||||
domain = application.domain
|
||||
|
||||
if domain and domain.has_gateway():
|
||||
gateway = domain.random_gateway()
|
||||
@@ -252,16 +261,12 @@ class SecretDetailMixin:
|
||||
'asset': asset,
|
||||
'application': application,
|
||||
'gateway': gateway,
|
||||
'domain': domain,
|
||||
'remote_app': remote_app,
|
||||
'actions': actions
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _get_asset_secret_detail(asset, user, system_user):
|
||||
from perms.utils.asset import get_asset_system_user_ids_with_actions_by_user
|
||||
systemuserid_actions_mapper = get_asset_system_user_ids_with_actions_by_user(user, asset)
|
||||
actions = systemuserid_actions_mapper.get(system_user.id, [])
|
||||
|
||||
def _get_asset_secret_detail(asset):
|
||||
gateway = None
|
||||
if asset and asset.domain and asset.domain.has_gateway():
|
||||
gateway = asset.domain.random_gateway()
|
||||
@@ -269,16 +274,22 @@ class SecretDetailMixin:
|
||||
return {
|
||||
'asset': asset,
|
||||
'application': None,
|
||||
'domain': asset.domain,
|
||||
'gateway': gateway,
|
||||
'remote_app': None,
|
||||
'actions': actions,
|
||||
}
|
||||
|
||||
@action(methods=['POST'], detail=False, permission_classes=[IsSuperUserOrAppUser], url_path='secret-info/detail')
|
||||
@action(methods=['POST'], detail=False, url_path='secret-info/detail')
|
||||
def get_secret_detail(self, request, *args, **kwargs):
|
||||
perm_required = 'authentication.view_connectiontokensecret'
|
||||
|
||||
# 非常重要的 api,再逻辑层再判断一下,双重保险
|
||||
if not request.user.has_perm(perm_required):
|
||||
raise PermissionDenied('Not allow to view secret')
|
||||
|
||||
token = request.data.get('token', '')
|
||||
try:
|
||||
value, user, system_user, asset, app, expired_at = self.valid_token(token)
|
||||
value, user, system_user, asset, app, expired_at, actions = self.valid_token(token)
|
||||
except serializers.ValidationError as e:
|
||||
post_auth_failed.send(
|
||||
sender=self.__class__, username='', request=self.request,
|
||||
@@ -286,20 +297,31 @@ class SecretDetailMixin:
|
||||
)
|
||||
raise e
|
||||
|
||||
data = dict(user=user, system_user=system_user, expired_at=expired_at)
|
||||
data = dict(
|
||||
id=token, secret=value.get('secret', ''),
|
||||
user=user, system_user=system_user,
|
||||
expired_at=expired_at, actions=actions
|
||||
)
|
||||
cmd_filter_kwargs = {
|
||||
'system_user_id': system_user.id,
|
||||
'user_id': user.id,
|
||||
}
|
||||
if asset:
|
||||
asset_detail = self._get_asset_secret_detail(asset, user=user, system_user=system_user)
|
||||
asset_detail = self._get_asset_secret_detail(asset)
|
||||
system_user.load_asset_more_auth(asset.id, user.username, user.id)
|
||||
data['type'] = 'asset'
|
||||
data.update(asset_detail)
|
||||
cmd_filter_kwargs['asset_id'] = asset.id
|
||||
else:
|
||||
app_detail = self._get_application_secret_detail(app)
|
||||
system_user.load_app_more_auth(app.id, user.username, user.id)
|
||||
data['type'] = 'application'
|
||||
data.update(app_detail)
|
||||
cmd_filter_kwargs['application_id'] = app.id
|
||||
|
||||
self.request.session['auth_backend'] = settings.AUTH_BACKEND_AUTH_TOKEN
|
||||
post_auth_success.send(sender=self.__class__, user=user, request=self.request, login_type='T')
|
||||
from assets.models import CommandFilterRule
|
||||
cmd_filter_rules = CommandFilterRule.get_queryset(**cmd_filter_kwargs)
|
||||
data['cmd_filter_rules'] = cmd_filter_rules
|
||||
|
||||
serializer = self.get_serializer(data)
|
||||
return Response(data=serializer.data, status=200)
|
||||
@@ -309,12 +331,18 @@ class UserConnectionTokenViewSet(
|
||||
RootOrgViewMixin, SerializerMixin, ClientProtocolMixin,
|
||||
SecretDetailMixin, GenericViewSet
|
||||
):
|
||||
permission_classes = (IsSuperUserOrAppUser,)
|
||||
serializer_classes = {
|
||||
'default': ConnectionTokenSerializer,
|
||||
'get_secret_detail': ConnectionTokenSecretSerializer,
|
||||
}
|
||||
CACHE_KEY_PREFIX = 'CONNECTION_TOKEN_{}'
|
||||
rbac_perms = {
|
||||
'GET': 'authentication.view_connectiontoken',
|
||||
'create': 'authentication.add_connectiontoken',
|
||||
'get_secret_detail': 'authentication.view_connectiontokensecret',
|
||||
'get_rdp_file': 'authentication.add_connectiontoken',
|
||||
'get_client_protocol_url': 'authentication.add_connectiontoken',
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def check_resource_permission(user, asset, application, system_user):
|
||||
@@ -332,15 +360,22 @@ class UserConnectionTokenViewSet(
|
||||
return True
|
||||
|
||||
def create_token(self, user, asset, application, system_user, ttl=5 * 60):
|
||||
if not self.request.user.is_superuser and user != self.request.user:
|
||||
raise PermissionDenied('Only super user can create user token')
|
||||
# 再次强调一下权限
|
||||
perm_required = 'authentication.add_superconnectiontoken'
|
||||
if user != self.request.user and not self.request.user.has_perm(perm_required):
|
||||
raise PermissionDenied('Only can create user token')
|
||||
self.check_resource_permission(user, asset, application, system_user)
|
||||
token = random_string(36)
|
||||
secret = random_string(16)
|
||||
value = {
|
||||
'id': token,
|
||||
'secret': secret,
|
||||
'user': str(user.id),
|
||||
'username': user.username,
|
||||
'system_user': str(system_user.id),
|
||||
'system_user_name': system_user.name
|
||||
'system_user_name': system_user.name,
|
||||
'created_by': str(self.request.user),
|
||||
'date_created': str(timezone.now())
|
||||
}
|
||||
|
||||
if asset:
|
||||
@@ -358,15 +393,20 @@ class UserConnectionTokenViewSet(
|
||||
|
||||
key = self.CACHE_KEY_PREFIX.format(token)
|
||||
cache.set(key, value, timeout=ttl)
|
||||
return token
|
||||
return token, secret
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
asset, application, system_user, user = self.get_request_resource(serializer)
|
||||
token = self.create_token(user, asset, application, system_user)
|
||||
return Response({"token": token}, status=201)
|
||||
token, secret = self.create_token(user, asset, application, system_user)
|
||||
tp = 'app' if application else 'asset'
|
||||
data = {
|
||||
"id": token, 'secret': secret,
|
||||
'type': tp, 'protocol': system_user.protocol
|
||||
}
|
||||
return Response(data, status=201)
|
||||
|
||||
def valid_token(self, token):
|
||||
from users.models import User
|
||||
@@ -398,15 +438,7 @@ class UserConnectionTokenViewSet(
|
||||
|
||||
if not has_perm:
|
||||
raise serializers.ValidationError('Permission expired or invalid')
|
||||
return value, user, system_user, asset, app, expired_at
|
||||
|
||||
def get_permissions(self):
|
||||
if self.action in ["create", "get_rdp_file"]:
|
||||
if self.request.data.get('user', None):
|
||||
self.permission_classes = (IsSuperUser,)
|
||||
else:
|
||||
self.permission_classes = (IsValidUser,)
|
||||
return super().get_permissions()
|
||||
return value, user, system_user, asset, app, expired_at, actions
|
||||
|
||||
def get(self, request):
|
||||
token = request.query_params.get('token')
|
||||
|
||||
@@ -5,7 +5,6 @@ from rest_framework.response import Response
|
||||
from users.permissions import IsAuthPasswdTimeValid
|
||||
from users.models import User
|
||||
from common.utils import get_logger
|
||||
from common.permissions import IsOrgAdmin
|
||||
from common.mixins.api import RoleUserMixin, RoleAdminMixin
|
||||
from authentication import errors
|
||||
|
||||
@@ -32,4 +31,4 @@ class DingTalkQRUnBindForUserApi(RoleUserMixin, DingTalkQRUnBindBase):
|
||||
|
||||
class DingTalkQRUnBindForAdminApi(RoleAdminMixin, DingTalkQRUnBindBase):
|
||||
user_id_url_kwarg = 'user_id'
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
@@ -5,7 +5,6 @@ from rest_framework.response import Response
|
||||
from users.permissions import IsAuthPasswdTimeValid
|
||||
from users.models import User
|
||||
from common.utils import get_logger
|
||||
from common.permissions import IsOrgAdmin
|
||||
from common.mixins.api import RoleUserMixin, RoleAdminMixin
|
||||
from authentication import errors
|
||||
|
||||
@@ -32,7 +31,6 @@ class FeiShuQRUnBindForUserApi(RoleUserMixin, FeiShuQRUnBindBase):
|
||||
|
||||
class FeiShuQRUnBindForAdminApi(RoleAdminMixin, FeiShuQRUnBindBase):
|
||||
user_id_url_kwarg = 'user_id'
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
|
||||
class FeiShuEventSubscriptionCallback(APIView):
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from rest_framework.generics import UpdateAPIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.permissions import AllowAny
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from common.utils import get_logger
|
||||
from common.permissions import IsOrgAdmin
|
||||
from .. import errors, mixins
|
||||
|
||||
__all__ = ['TicketStatusApi']
|
||||
|
||||
@@ -39,14 +39,6 @@ class MFASendCodeApi(AuthMixin, CreateAPIView):
|
||||
username = ''
|
||||
ip = ''
|
||||
|
||||
def get_user_from_db(self, username):
|
||||
try:
|
||||
user = get_object_or_404(User, username=username)
|
||||
return user
|
||||
except Exception as e:
|
||||
self.incr_mfa_failed_time(username, self.ip)
|
||||
raise e
|
||||
|
||||
def get_user_from_db(self, username):
|
||||
"""避免暴力测试用户名"""
|
||||
ip = self.get_request_ip()
|
||||
|
||||
@@ -13,7 +13,7 @@ from common.utils.timezone import utc_now
|
||||
from common.const.http import POST, GET
|
||||
from common.drf.api import JMSGenericViewSet
|
||||
from common.drf.serializers import EmptySerializer
|
||||
from common.permissions import IsSuperUser
|
||||
from common.permissions import OnlySuperUser
|
||||
from common.utils import reverse
|
||||
from users.models import User
|
||||
from ..serializers import SSOTokenSerializer
|
||||
@@ -32,9 +32,8 @@ class SSOViewSet(AuthMixin, JMSGenericViewSet):
|
||||
'login_url': SSOTokenSerializer,
|
||||
'login': EmptySerializer
|
||||
}
|
||||
permission_classes = (IsSuperUser,)
|
||||
|
||||
@action(methods=[POST], detail=False, permission_classes=[IsSuperUser], url_path='login-url')
|
||||
@action(methods=[POST], detail=False, permission_classes=[OnlySuperUser], url_path='login-url')
|
||||
def login_url(self, request, *args, **kwargs):
|
||||
if not settings.AUTH_SSO:
|
||||
raise SSOAuthClosed()
|
||||
@@ -84,6 +83,6 @@ class SSOViewSet(AuthMixin, JMSGenericViewSet):
|
||||
return HttpResponseRedirect(next_url)
|
||||
|
||||
user = token.user
|
||||
login(self.request, user, 'authentication.backends.api.SSOAuthentication')
|
||||
login(self.request, user, settings.AUTH_BACKEND_SSO)
|
||||
self.send_auth_signal(success=True, user=user)
|
||||
return HttpResponseRedirect(next_url)
|
||||
|
||||
@@ -5,7 +5,6 @@ from rest_framework.response import Response
|
||||
from users.permissions import IsAuthPasswdTimeValid
|
||||
from users.models import User
|
||||
from common.utils import get_logger
|
||||
from common.permissions import IsOrgAdmin
|
||||
from common.mixins.api import RoleUserMixin, RoleAdminMixin
|
||||
from authentication import errors
|
||||
|
||||
@@ -32,4 +31,4 @@ class WeComQRUnBindForUserApi(RoleUserMixin, WeComQRUnBindBase):
|
||||
|
||||
class WeComQRUnBindForAdminApi(RoleAdminMixin, WeComQRUnBindBase):
|
||||
user_id_url_kwarg = 'user_id'
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
from django.apps import AppConfig
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
class AuthenticationConfig(AppConfig):
|
||||
name = 'authentication'
|
||||
verbose_name = _('Authentication')
|
||||
|
||||
def ready(self):
|
||||
from . import signals_handlers
|
||||
from . import signal_handlers
|
||||
from . import notifications
|
||||
super().ready()
|
||||
|
||||
|
||||
56
apps/authentication/backends/base.py
Normal file
56
apps/authentication/backends/base.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from django.contrib.auth.backends import BaseBackend
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
|
||||
from users.models import User
|
||||
from common.utils import get_logger
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class JMSBaseAuthBackend:
|
||||
|
||||
@staticmethod
|
||||
def is_enabled():
|
||||
return True
|
||||
|
||||
def has_perm(self, user_obj, perm, obj=None):
|
||||
return False
|
||||
|
||||
def user_can_authenticate(self, user):
|
||||
"""
|
||||
Reject users with is_valid=False. Custom user models that don't have
|
||||
that attribute are allowed.
|
||||
"""
|
||||
# 在 check_user_auth 中进行了校验,可以返回对应的错误信息
|
||||
# is_valid = getattr(user, 'is_valid', None)
|
||||
# return is_valid or is_valid is None
|
||||
return True
|
||||
|
||||
# allow user to authenticate
|
||||
def username_allow_authenticate(self, username):
|
||||
return self.allow_authenticate(username=username)
|
||||
|
||||
def user_allow_authenticate(self, user):
|
||||
return self.allow_authenticate(user=user)
|
||||
|
||||
def allow_authenticate(self, user=None, username=None):
|
||||
if user:
|
||||
allowed_backend_paths = user.get_allowed_auth_backend_paths()
|
||||
else:
|
||||
allowed_backend_paths = User.get_user_allowed_auth_backend_paths(username)
|
||||
if allowed_backend_paths is None:
|
||||
# 特殊值 None 表示没有限制
|
||||
return True
|
||||
backend_name = self.__class__.__name__
|
||||
allowed_backend_names = [path.split('.')[-1] for path in allowed_backend_paths]
|
||||
allow = backend_name in allowed_backend_names
|
||||
if not allow:
|
||||
info = 'User {} skip authentication backend {}, because it not in {}'
|
||||
info = info.format(username, backend_name, ','.join(allowed_backend_names))
|
||||
logger.debug(info)
|
||||
return allow
|
||||
|
||||
|
||||
class JMSModelBackend(JMSBaseAuthBackend, ModelBackend):
|
||||
pass
|
||||
@@ -1,3 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
# 保证 utils 中的模块进行初始化
|
||||
from . import utils
|
||||
from .backends import *
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django_cas_ng.backends import CASBackend as _CASBackend
|
||||
from django.conf import settings
|
||||
|
||||
from ..base import JMSBaseAuthBackend
|
||||
|
||||
__all__ = ['CASBackend']
|
||||
|
||||
|
||||
class CASBackend(_CASBackend):
|
||||
def user_can_authenticate(self, user):
|
||||
return True
|
||||
class CASBackend(JMSBaseAuthBackend, _CASBackend):
|
||||
@staticmethod
|
||||
def is_enabled():
|
||||
return settings.AUTH_CAS
|
||||
|
||||
32
apps/authentication/backends/cas/utils.py
Normal file
32
apps/authentication/backends/cas/utils.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from django_cas_ng import utils
|
||||
from django_cas_ng.utils import (
|
||||
django_settings, get_protocol,
|
||||
urllib_parse, REDIRECT_FIELD_NAME, get_redirect_url
|
||||
)
|
||||
|
||||
|
||||
def get_service_url(request, redirect_to=None):
|
||||
"""
|
||||
重写 get_service url 方法, CAS_ROOT_PROXIED_AS 为空时, 支持跳转回当前访问的域名地址
|
||||
"""
|
||||
"""Generates application django service URL for CAS"""
|
||||
if getattr(django_settings, 'CAS_ROOT_PROXIED_AS', None):
|
||||
service = django_settings.CAS_ROOT_PROXIED_AS + request.path
|
||||
else:
|
||||
protocol = get_protocol(request)
|
||||
host = request.get_host()
|
||||
service = urllib_parse.urlunparse(
|
||||
(protocol, host, request.path, '', '', ''),
|
||||
)
|
||||
if not django_settings.CAS_STORE_NEXT:
|
||||
if '?' in service:
|
||||
service += '&'
|
||||
else:
|
||||
service += '?'
|
||||
service += urllib_parse.urlencode({
|
||||
REDIRECT_FIELD_NAME: redirect_to or get_redirect_url(request)
|
||||
})
|
||||
return service
|
||||
|
||||
|
||||
utils.get_service_url = get_service_url
|
||||
@@ -8,13 +8,14 @@ from django.core.cache import cache
|
||||
from django.utils.translation import ugettext as _
|
||||
from six import text_type
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
|
||||
from rest_framework import HTTP_HEADER_ENCODING
|
||||
from rest_framework import authentication, exceptions
|
||||
from common.auth import signature
|
||||
|
||||
from common.utils import get_object_or_none, make_signature, http_to_unixtime
|
||||
from ..models import AccessKey, PrivateToken
|
||||
from .base import JMSBaseAuthBackend, JMSModelBackend
|
||||
|
||||
|
||||
UserModel = get_user_model()
|
||||
@@ -28,18 +29,6 @@ def get_request_date_header(request):
|
||||
return date
|
||||
|
||||
|
||||
class JMSModelBackend(ModelBackend):
|
||||
def user_can_authenticate(self, user):
|
||||
return True
|
||||
|
||||
def get_user(self, user_id):
|
||||
try:
|
||||
user = UserModel._default_manager.get(pk=user_id)
|
||||
except UserModel.DoesNotExist:
|
||||
return None
|
||||
return user if user.is_valid else None
|
||||
|
||||
|
||||
class AccessKeyAuthentication(authentication.BaseAuthentication):
|
||||
"""App使用Access key进行签名认证, 目前签名算法比较简单,
|
||||
app注册或者手动建立后,会生成 access_key_id 和 access_key_secret,
|
||||
@@ -164,7 +153,7 @@ class AccessTokenAuthentication(authentication.BaseAuthentication):
|
||||
return self.keyword
|
||||
|
||||
|
||||
class PrivateTokenAuthentication(authentication.TokenAuthentication):
|
||||
class PrivateTokenAuthentication(JMSBaseAuthBackend, authentication.TokenAuthentication):
|
||||
model = PrivateToken
|
||||
|
||||
|
||||
@@ -212,46 +201,3 @@ class SignatureAuthentication(signature.SignatureAuthentication):
|
||||
except AccessKey.DoesNotExist:
|
||||
return None, None
|
||||
|
||||
|
||||
class SSOAuthentication(JMSModelBackend):
|
||||
"""
|
||||
什么也不做呀😺
|
||||
"""
|
||||
|
||||
def authenticate(self, request, sso_token=None, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class WeComAuthentication(JMSModelBackend):
|
||||
"""
|
||||
什么也不做呀😺
|
||||
"""
|
||||
|
||||
def authenticate(self, request, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class DingTalkAuthentication(JMSModelBackend):
|
||||
"""
|
||||
什么也不做呀😺
|
||||
"""
|
||||
|
||||
def authenticate(self, request, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class FeiShuAuthentication(JMSModelBackend):
|
||||
"""
|
||||
什么也不做呀😺
|
||||
"""
|
||||
|
||||
def authenticate(self, request, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class AuthorizationTokenAuthentication(JMSModelBackend):
|
||||
"""
|
||||
什么也不做呀😺
|
||||
"""
|
||||
def authenticate(self, request, **kwargs):
|
||||
pass
|
||||
@@ -1,43 +1,38 @@
|
||||
# coding:utf-8
|
||||
#
|
||||
|
||||
import warnings
|
||||
import ldap
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
|
||||
from django_auth_ldap.backend import _LDAPUser, LDAPBackend, LDAPSettings
|
||||
from django_auth_ldap.backend import _LDAPUser, LDAPBackend
|
||||
from django_auth_ldap.config import _LDAPConfig, LDAPSearch, LDAPSearchUnion
|
||||
|
||||
from users.utils import construct_user_email
|
||||
from common.const import LDAP_AD_ACCOUNT_DISABLE
|
||||
from .base import JMSBaseAuthBackend
|
||||
|
||||
logger = _LDAPConfig.get_logger()
|
||||
|
||||
|
||||
class LDAPAuthorizationBackend(LDAPBackend):
|
||||
class LDAPAuthorizationBackend(JMSBaseAuthBackend, LDAPBackend):
|
||||
"""
|
||||
Override this class to override _LDAPUser to LDAPUser
|
||||
"""
|
||||
@staticmethod
|
||||
def user_can_authenticate(user):
|
||||
"""
|
||||
Reject users with is_active=False. Custom user models that don't have
|
||||
that attribute are allowed.
|
||||
"""
|
||||
is_valid = getattr(user, 'is_valid', None)
|
||||
return is_valid or is_valid is None
|
||||
def is_enabled():
|
||||
return settings.AUTH_LDAP
|
||||
|
||||
def get_or_build_user(self, username, ldap_user):
|
||||
"""
|
||||
This must return a (User, built) 2-tuple for the given LDAP user.
|
||||
This must return a (User, built) 2-tuple for the given LDAP user.
|
||||
|
||||
username is the Django-friendly username of the user. ldap_user.dn is
|
||||
the user's DN and ldap_user.attrs contains all of their LDAP
|
||||
attributes.
|
||||
username is the Django-friendly username of the user. ldap_user.dn is
|
||||
the user's DN and ldap_user.attrs contains all of their LDAP
|
||||
attributes.
|
||||
|
||||
The returned User object may be an unsaved model instance.
|
||||
The returned User object may be an unsaved model instance.
|
||||
|
||||
"""
|
||||
"""
|
||||
model = self.get_user_model()
|
||||
|
||||
if self.settings.USER_QUERY_FIELD:
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from .backends import *
|
||||
|
||||
290
apps/authentication/backends/oidc/backends.py
Normal file
290
apps/authentication/backends/oidc/backends.py
Normal file
@@ -0,0 +1,290 @@
|
||||
"""
|
||||
OpenID Connect relying party (RP) authentication backends
|
||||
=========================================================
|
||||
|
||||
This modules defines backends allowing to authenticate a user using a specific token endpoint
|
||||
of an OpenID Connect provider (OP).
|
||||
|
||||
"""
|
||||
|
||||
import base64
|
||||
import requests
|
||||
from rest_framework.exceptions import ParseError
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.db import transaction
|
||||
from django.urls import reverse
|
||||
from django.conf import settings
|
||||
|
||||
from common.utils import get_logger
|
||||
|
||||
from ..base import JMSBaseAuthBackend
|
||||
from .utils import validate_and_return_id_token, build_absolute_uri
|
||||
from .decorator import ssl_verification
|
||||
from .signals import (
|
||||
openid_create_or_update_user, openid_user_login_failed, openid_user_login_success
|
||||
)
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
__all__ = ['OIDCAuthCodeBackend', 'OIDCAuthPasswordBackend']
|
||||
|
||||
|
||||
class UserMixin:
|
||||
|
||||
@transaction.atomic
|
||||
def get_or_create_user_from_claims(self, request, claims):
|
||||
log_prompt = "Get or Create user from claims [ActionForUser]: {}"
|
||||
logger.debug(log_prompt.format('start'))
|
||||
|
||||
sub = claims['sub']
|
||||
name = claims.get('name', sub)
|
||||
username = claims.get('preferred_username', sub)
|
||||
email = claims.get('email', "{}@{}".format(username, 'jumpserver.openid'))
|
||||
logger.debug(
|
||||
log_prompt.format(
|
||||
"sub: {}|name: {}|username: {}|email: {}".format(sub, name, username, email)
|
||||
)
|
||||
)
|
||||
|
||||
user, created = get_user_model().objects.get_or_create(
|
||||
username=username, defaults={"name": name, "email": email}
|
||||
)
|
||||
logger.debug(log_prompt.format("user: {}|created: {}".format(user, created)))
|
||||
logger.debug(log_prompt.format("Send signal => openid create or update user"))
|
||||
openid_create_or_update_user.send(
|
||||
sender=self.__class__, request=request, user=user, created=created,
|
||||
name=name, username=username, email=email
|
||||
)
|
||||
return user, created
|
||||
|
||||
|
||||
class OIDCBaseBackend(UserMixin, JMSBaseAuthBackend, ModelBackend):
|
||||
|
||||
@staticmethod
|
||||
def is_enabled():
|
||||
return settings.AUTH_OPENID
|
||||
|
||||
|
||||
class OIDCAuthCodeBackend(OIDCBaseBackend):
|
||||
""" Allows to authenticate users using an OpenID Connect Provider (OP).
|
||||
|
||||
This authentication backend is able to authenticate users in the case of the OpenID Connect
|
||||
Authorization Code flow. The ``authenticate`` method provided by this backend is likely to be
|
||||
called when the callback URL is requested by the OpenID Connect Provider (OP). Thus it will
|
||||
call the OIDC provider again in order to request a valid token using the authorization code that
|
||||
should be available in the request parameters associated with the callback call.
|
||||
|
||||
"""
|
||||
|
||||
@ssl_verification
|
||||
def authenticate(self, request, nonce=None, **kwargs):
|
||||
""" Authenticates users in case of the OpenID Connect Authorization code flow. """
|
||||
log_prompt = "Process authenticate [OIDCAuthCodeBackend]: {}"
|
||||
logger.debug(log_prompt.format('start'))
|
||||
|
||||
# NOTE: the request object is mandatory to perform the authentication using an authorization
|
||||
# code provided by the OIDC supplier.
|
||||
if (nonce is None and settings.AUTH_OPENID_USE_NONCE) or request is None:
|
||||
logger.debug(log_prompt.format('Request or nonce value is missing'))
|
||||
return
|
||||
|
||||
# Fetches required GET parameters from the HTTP request object.
|
||||
state = request.GET.get('state')
|
||||
code = request.GET.get('code')
|
||||
|
||||
# Don't go further if the state value or the authorization code is not present in the GET
|
||||
# parameters because we won't be able to get a valid token for the user in that case.
|
||||
if (state is None and settings.AUTH_OPENID_USE_STATE) or code is None:
|
||||
logger.debug(log_prompt.format('Authorization code or state value is missing'))
|
||||
raise SuspiciousOperation('Authorization code or state value is missing')
|
||||
|
||||
# Prepares the token payload that will be used to request an authentication token to the
|
||||
# token endpoint of the OIDC provider.
|
||||
logger.debug(log_prompt.format('Prepares token payload'))
|
||||
token_payload = {
|
||||
'client_id': settings.AUTH_OPENID_CLIENT_ID,
|
||||
'client_secret': settings.AUTH_OPENID_CLIENT_SECRET,
|
||||
'grant_type': 'authorization_code',
|
||||
'code': code,
|
||||
'redirect_uri': build_absolute_uri(
|
||||
request, path=reverse(settings.AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME)
|
||||
)
|
||||
}
|
||||
|
||||
# Prepares the token headers that will be used to request an authentication token to the
|
||||
# token endpoint of the OIDC provider.
|
||||
logger.debug(log_prompt.format('Prepares token headers'))
|
||||
basic_token = "{}:{}".format(settings.AUTH_OPENID_CLIENT_ID, settings.AUTH_OPENID_CLIENT_SECRET)
|
||||
headers = {"Authorization": "Basic {}".format(base64.b64encode(basic_token.encode()).decode())}
|
||||
|
||||
# Calls the token endpoint.
|
||||
logger.debug(log_prompt.format('Call the token endpoint'))
|
||||
token_response = requests.post(
|
||||
settings.AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT, data=token_payload, headers=headers
|
||||
)
|
||||
try:
|
||||
token_response.raise_for_status()
|
||||
token_response_data = token_response.json()
|
||||
except Exception as e:
|
||||
error = "Json token response error, token response " \
|
||||
"content is: {}, error is: {}".format(token_response.content, str(e))
|
||||
logger.debug(log_prompt.format(error))
|
||||
raise ParseError(error)
|
||||
|
||||
# Validates the token.
|
||||
logger.debug(log_prompt.format('Validate ID Token'))
|
||||
raw_id_token = token_response_data.get('id_token')
|
||||
id_token = validate_and_return_id_token(raw_id_token, nonce)
|
||||
if id_token is None:
|
||||
logger.debug(log_prompt.format(
|
||||
'ID Token is missing, raw id token is: {}'.format(raw_id_token))
|
||||
)
|
||||
return
|
||||
|
||||
# Retrieves the access token and refresh token.
|
||||
access_token = token_response_data.get('access_token')
|
||||
refresh_token = token_response_data.get('refresh_token')
|
||||
|
||||
# Stores the ID token, the related access token and the refresh token in the session.
|
||||
request.session['oidc_auth_id_token'] = raw_id_token
|
||||
request.session['oidc_auth_access_token'] = access_token
|
||||
request.session['oidc_auth_refresh_token'] = refresh_token
|
||||
|
||||
# If the id_token contains userinfo scopes and claims we don't have to hit the userinfo
|
||||
# endpoint.
|
||||
# https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
|
||||
if settings.AUTH_OPENID_ID_TOKEN_INCLUDE_CLAIMS:
|
||||
logger.debug(log_prompt.format('ID Token in claims'))
|
||||
claims = id_token
|
||||
else:
|
||||
# Fetches the claims (user information) from the userinfo endpoint provided by the OP.
|
||||
logger.debug(log_prompt.format('Fetches the claims from the userinfo endpoint'))
|
||||
claims_response = requests.get(
|
||||
settings.AUTH_OPENID_PROVIDER_USERINFO_ENDPOINT,
|
||||
headers={'Authorization': 'Bearer {0}'.format(access_token)}
|
||||
)
|
||||
try:
|
||||
claims_response.raise_for_status()
|
||||
claims = claims_response.json()
|
||||
except Exception as e:
|
||||
error = "Json claims response error, claims response " \
|
||||
"content is: {}, error is: {}".format(claims_response.content, str(e))
|
||||
logger.debug(log_prompt.format(error))
|
||||
raise ParseError(error)
|
||||
|
||||
logger.debug(log_prompt.format('Get or create user from claims'))
|
||||
user, created = self.get_or_create_user_from_claims(request, claims)
|
||||
|
||||
logger.debug(log_prompt.format('Update or create oidc user'))
|
||||
|
||||
if self.user_can_authenticate(user):
|
||||
logger.debug(log_prompt.format('OpenID user login success'))
|
||||
logger.debug(log_prompt.format('Send signal => openid user login success'))
|
||||
openid_user_login_success.send(sender=self.__class__, request=request, user=user)
|
||||
return user
|
||||
else:
|
||||
logger.debug(log_prompt.format('OpenID user login failed'))
|
||||
logger.debug(log_prompt.format('Send signal => openid user login failed'))
|
||||
openid_user_login_failed.send(
|
||||
sender=self.__class__, request=request, username=user.username,
|
||||
reason="User is invalid"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
class OIDCAuthPasswordBackend(OIDCBaseBackend):
|
||||
|
||||
@ssl_verification
|
||||
def authenticate(self, request, username=None, password=None, **kwargs):
|
||||
try:
|
||||
return self._authenticate(request, username, password, **kwargs)
|
||||
except Exception as e:
|
||||
error = f'Authenticate exception: {e}'
|
||||
logger.error(error, exc_info=True)
|
||||
return
|
||||
|
||||
def _authenticate(self, request, username=None, password=None, **kwargs):
|
||||
"""
|
||||
https://oauth.net/2/
|
||||
https://aaronparecki.com/oauth-2-simplified/#password
|
||||
"""
|
||||
log_prompt = "Process authenticate [OIDCAuthPasswordBackend]: {}"
|
||||
logger.debug(log_prompt.format('start'))
|
||||
request_timeout = 15
|
||||
|
||||
if not username or not password:
|
||||
logger.debug(log_prompt.format('Username or password is missing'))
|
||||
return
|
||||
|
||||
# Prepares the token payload that will be used to request an authentication token to the
|
||||
# token endpoint of the OIDC provider.
|
||||
logger.debug(log_prompt.format('Prepares token payload'))
|
||||
token_payload = {
|
||||
'client_id': settings.AUTH_OPENID_CLIENT_ID,
|
||||
'client_secret': settings.AUTH_OPENID_CLIENT_SECRET,
|
||||
'grant_type': 'password',
|
||||
'username': username,
|
||||
'password': password,
|
||||
}
|
||||
|
||||
# Calls the token endpoint.
|
||||
logger.debug(log_prompt.format('Call the token endpoint'))
|
||||
token_response = requests.post(settings.AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT, data=token_payload, timeout=request_timeout)
|
||||
try:
|
||||
token_response.raise_for_status()
|
||||
token_response_data = token_response.json()
|
||||
except Exception as e:
|
||||
error = "Json token response error, token response " \
|
||||
"content is: {}, error is: {}".format(token_response.content, str(e))
|
||||
logger.debug(log_prompt.format(error))
|
||||
logger.debug(log_prompt.format('Send signal => openid user login failed'))
|
||||
openid_user_login_failed.send(
|
||||
sender=self.__class__, request=request, username=username, reason=error
|
||||
)
|
||||
return
|
||||
|
||||
# Retrieves the access token
|
||||
access_token = token_response_data.get('access_token')
|
||||
|
||||
# Fetches the claims (user information) from the userinfo endpoint provided by the OP.
|
||||
logger.debug(log_prompt.format('Fetches the claims from the userinfo endpoint'))
|
||||
claims_response = requests.get(
|
||||
settings.AUTH_OPENID_PROVIDER_USERINFO_ENDPOINT,
|
||||
headers={'Authorization': 'Bearer {0}'.format(access_token)},
|
||||
timeout=request_timeout
|
||||
)
|
||||
try:
|
||||
claims_response.raise_for_status()
|
||||
claims = claims_response.json()
|
||||
except Exception as e:
|
||||
error = "Json claims response error, claims response " \
|
||||
"content is: {}, error is: {}".format(claims_response.content, str(e))
|
||||
logger.debug(log_prompt.format(error))
|
||||
logger.debug(log_prompt.format('Send signal => openid user login failed'))
|
||||
openid_user_login_failed.send(
|
||||
sender=self.__class__, request=request, username=username, reason=error
|
||||
)
|
||||
return
|
||||
|
||||
logger.debug(log_prompt.format('Get or create user from claims'))
|
||||
user, created = self.get_or_create_user_from_claims(request, claims)
|
||||
|
||||
logger.debug(log_prompt.format('Update or create oidc user'))
|
||||
|
||||
if self.user_can_authenticate(user):
|
||||
logger.debug(log_prompt.format('OpenID user login success'))
|
||||
logger.debug(log_prompt.format('Send signal => openid user login success'))
|
||||
openid_user_login_success.send(
|
||||
sender=self.__class__, request=request, user=user
|
||||
)
|
||||
return user
|
||||
else:
|
||||
logger.debug(log_prompt.format('OpenID user login failed'))
|
||||
logger.debug(log_prompt.format('Send signal => openid user login failed'))
|
||||
openid_user_login_failed.send(
|
||||
sender=self.__class__, request=request, username=username, reason="User is invalid"
|
||||
)
|
||||
return None
|
||||
|
||||
60
apps/authentication/backends/oidc/decorator.py
Normal file
60
apps/authentication/backends/oidc/decorator.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
|
||||
import warnings
|
||||
import contextlib
|
||||
import requests
|
||||
|
||||
from django.conf import settings
|
||||
from urllib3.exceptions import InsecureRequestWarning
|
||||
|
||||
from .utils import get_logger
|
||||
|
||||
__all__ = ['ssl_verification']
|
||||
|
||||
old_merge_environment_settings = requests.Session.merge_environment_settings
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def no_ssl_verification():
|
||||
"""
|
||||
https://stackoverflow.com/questions/15445981/
|
||||
how-do-i-disable-the-security-certificate-check-in-python-requests
|
||||
"""
|
||||
opened_adapters = set()
|
||||
|
||||
def merge_environment_settings(self, url, proxies, stream, verify, cert):
|
||||
# Verification happens only once per connection so we need to close
|
||||
# all the opened adapters once we're done. Otherwise, the effects of
|
||||
# verify=False persist beyond the end of this context manager.
|
||||
opened_adapters.add(self.get_adapter(url))
|
||||
_settings = old_merge_environment_settings(
|
||||
self, url, proxies, stream, verify, cert
|
||||
)
|
||||
_settings['verify'] = False
|
||||
return _settings
|
||||
|
||||
requests.Session.merge_environment_settings = merge_environment_settings
|
||||
try:
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter('ignore', InsecureRequestWarning)
|
||||
yield
|
||||
finally:
|
||||
requests.Session.merge_environment_settings = old_merge_environment_settings
|
||||
for adapter in opened_adapters:
|
||||
try:
|
||||
adapter.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def ssl_verification(func):
|
||||
def wrapper(*args, **kwargs):
|
||||
if not settings.AUTH_OPENID_IGNORE_SSL_VERIFICATION:
|
||||
return func(*args, **kwargs)
|
||||
with no_ssl_verification():
|
||||
return func(*args, **kwargs)
|
||||
return wrapper
|
||||
@@ -1,10 +1,106 @@
|
||||
from jms_oidc_rp.middleware import OIDCRefreshIDTokenMiddleware as _OIDCRefreshIDTokenMiddleware
|
||||
import time
|
||||
import requests
|
||||
import requests.exceptions
|
||||
|
||||
from django.core.exceptions import MiddlewareNotUsed
|
||||
from django.conf import settings
|
||||
from django.contrib import auth
|
||||
from common.utils import get_logger
|
||||
|
||||
from .utils import validate_and_return_id_token
|
||||
from .decorator import ssl_verification
|
||||
|
||||
|
||||
class OIDCRefreshIDTokenMiddleware(_OIDCRefreshIDTokenMiddleware):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class OIDCRefreshIDTokenMiddleware:
|
||||
""" Allows to periodically refresh the ID token associated with the authenticated user. """
|
||||
|
||||
def __init__(self, get_response):
|
||||
if not settings.AUTH_OPENID:
|
||||
raise MiddlewareNotUsed
|
||||
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
# Refreshes tokens only in the applicable cases.
|
||||
if request.method == 'GET' and not request.is_ajax() and request.user.is_authenticated and settings.AUTH_OPENID:
|
||||
self.refresh_token(request)
|
||||
response = self.get_response(request)
|
||||
return response
|
||||
|
||||
@ssl_verification
|
||||
def refresh_token(self, request):
|
||||
""" Refreshes the token of the current user. """
|
||||
|
||||
log_prompt = "Process refresh Token: {}"
|
||||
# logger.debug(log_prompt.format('Start'))
|
||||
|
||||
# NOTE: SHARE_SESSION is False means that the user does not share sessions
|
||||
# with other applications
|
||||
if not settings.AUTH_OPENID_SHARE_SESSION:
|
||||
logger.debug(log_prompt.format('Not share session'))
|
||||
return
|
||||
|
||||
# NOTE: no refresh token in the session means that the user wasn't authentified using the
|
||||
# OpenID Connect provider (OP).
|
||||
refresh_token = request.session.get('oidc_auth_refresh_token')
|
||||
if refresh_token is None:
|
||||
logger.debug(log_prompt.format('Refresh token is missing'))
|
||||
return
|
||||
|
||||
id_token_exp_timestamp = request.session.get('oidc_auth_id_token_exp_timestamp', None)
|
||||
now_timestamp = time.time()
|
||||
# Returns immediately if the token is still valid.
|
||||
if id_token_exp_timestamp is not None and id_token_exp_timestamp > now_timestamp:
|
||||
# logger.debug(log_prompt.format('Returns immediately because token is still valid'))
|
||||
return
|
||||
|
||||
# Prepares the token payload that will be used to request a new token from the token
|
||||
# endpoint.
|
||||
refresh_token = request.session.pop('oidc_auth_refresh_token')
|
||||
token_payload = {
|
||||
'client_id': settings.AUTH_OPENID_CLIENT_ID,
|
||||
'client_secret': settings.AUTH_OPENID_CLIENT_SECRET,
|
||||
'grant_type': 'refresh_token',
|
||||
'refresh_token': refresh_token,
|
||||
'scope': settings.AUTH_OPENID_SCOPES,
|
||||
}
|
||||
|
||||
# Calls the token endpoint.
|
||||
logger.debug(log_prompt.format('Calls the token endpoint'))
|
||||
token_response = requests.post(settings.AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT, data=token_payload)
|
||||
try:
|
||||
token_response.raise_for_status()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
logger.debug(log_prompt.format('Request exception http error: {}'.format(str(e))))
|
||||
logger.debug(log_prompt.format('Logout'))
|
||||
auth.logout(request)
|
||||
return
|
||||
token_response_data = token_response.json()
|
||||
|
||||
# Validates the token.
|
||||
logger.debug(log_prompt.format('Validate ID Token'))
|
||||
raw_id_token = token_response_data.get('id_token')
|
||||
id_token = validate_and_return_id_token(raw_id_token, validate_nonce=False)
|
||||
|
||||
# If the token cannot be validated we have to log out the current user.
|
||||
if id_token is None:
|
||||
logger.debug(log_prompt.format('ID Token is None'))
|
||||
auth.logout(request)
|
||||
logger.debug(log_prompt.format('Logout'))
|
||||
return
|
||||
|
||||
# Retrieves the access token and refresh token.
|
||||
access_token = token_response_data.get('access_token')
|
||||
refresh_token = token_response_data.get('refresh_token')
|
||||
|
||||
# Stores the ID token, the related access token and the refresh token in the session.
|
||||
request.session['oidc_auth_id_token'] = raw_id_token
|
||||
request.session['oidc_auth_access_token'] = access_token
|
||||
request.session['oidc_auth_refresh_token'] = refresh_token
|
||||
|
||||
# Saves the new expiration timestamp.
|
||||
request.session['oidc_auth_id_token_exp_timestamp'] = \
|
||||
time.time() + settings.AUTH_OPENID_ID_TOKEN_MAX_AGE
|
||||
|
||||
18
apps/authentication/backends/oidc/signals.py
Normal file
18
apps/authentication/backends/oidc/signals.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
OpenID Connect relying party (RP) signals
|
||||
=========================================
|
||||
|
||||
This modules defines Django signals that can be triggered during the OpenID Connect
|
||||
authentication process.
|
||||
|
||||
"""
|
||||
|
||||
from django.dispatch import Signal
|
||||
|
||||
|
||||
openid_create_or_update_user = Signal(
|
||||
providing_args=['request', 'user', 'created', 'name', 'username', 'email']
|
||||
)
|
||||
openid_user_login_success = Signal(providing_args=['request', 'user'])
|
||||
openid_user_login_failed = Signal(providing_args=['request', 'username', 'reason'])
|
||||
|
||||
20
apps/authentication/backends/oidc/urls.py
Normal file
20
apps/authentication/backends/oidc/urls.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""
|
||||
OpenID Connect relying party (RP) URLs
|
||||
======================================
|
||||
|
||||
This modules defines the URLs allowing to perform OpenID Connect flows on a Relying Party (RP).
|
||||
It defines three main endpoints: the authentication request endpoint, the authentication
|
||||
callback endpoint and the end session endpoint.
|
||||
|
||||
"""
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('login/', views.OIDCAuthRequestView.as_view(), name='login'),
|
||||
path('callback/', views.OIDCAuthCallbackView.as_view(), name='login-callback'),
|
||||
path('logout/', views.OIDCEndSessionView.as_view(), name='logout'),
|
||||
]
|
||||
126
apps/authentication/backends/oidc/utils.py
Normal file
126
apps/authentication/backends/oidc/utils.py
Normal file
@@ -0,0 +1,126 @@
|
||||
"""
|
||||
OpenID Connect relying party (RP) utilities
|
||||
===========================================
|
||||
|
||||
This modules defines utilities allowing to manipulate ID tokens and other common helpers.
|
||||
|
||||
"""
|
||||
|
||||
import datetime as dt
|
||||
from calendar import timegm
|
||||
from urllib.parse import urlparse, urljoin
|
||||
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.utils.encoding import force_bytes, smart_bytes
|
||||
from jwkest import JWKESTException
|
||||
from jwkest.jwk import KEYS
|
||||
from jwkest.jws import JWS
|
||||
from django.conf import settings
|
||||
|
||||
from common.utils import get_logger
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
def validate_and_return_id_token(jws, nonce=None, validate_nonce=True):
|
||||
""" Validates the id_token according to the OpenID Connect specification. """
|
||||
log_prompt = "Validate ID Token: {}"
|
||||
logger.debug(log_prompt.format('Get shared key'))
|
||||
shared_key = settings.AUTH_OPENID_CLIENT_ID \
|
||||
if settings.AUTH_OPENID_PROVIDER_SIGNATURE_ALG == 'HS256' \
|
||||
else settings.AUTH_OPENID_PROVIDER_SIGNATURE_KEY # RS256
|
||||
|
||||
try:
|
||||
# Decodes the JSON Web Token and raise an error if the signature is invalid.
|
||||
logger.debug(log_prompt.format('Verify compact jwk'))
|
||||
id_token = JWS().verify_compact(force_bytes(jws), _get_jwks_keys(shared_key))
|
||||
except JWKESTException as e:
|
||||
logger.debug(log_prompt.format('Verify compact jwkest exception: {}'.format(str(e))))
|
||||
return
|
||||
|
||||
# Validates the claims embedded in the id_token.
|
||||
logger.debug(log_prompt.format('Validate claims'))
|
||||
_validate_claims(id_token, nonce=nonce, validate_nonce=validate_nonce)
|
||||
|
||||
return id_token
|
||||
|
||||
|
||||
def _get_jwks_keys(shared_key):
|
||||
""" Returns JWKS keys used to decrypt id_token values. """
|
||||
# The OpenID Connect Provider (OP) uses RSA keys to sign/enrypt ID tokens and generate public
|
||||
# keys allowing to decrypt them. These public keys are exposed through the 'jwks_uri' and should
|
||||
# be used to decrypt the JWS - JSON Web Signature.
|
||||
log_prompt = "Get jwks keys: {}"
|
||||
logger.debug(log_prompt.format('Start'))
|
||||
jwks_keys = KEYS()
|
||||
logger.debug(log_prompt.format('Load from provider jwks endpoint'))
|
||||
jwks_keys.load_from_url(settings.AUTH_OPENID_PROVIDER_JWKS_ENDPOINT)
|
||||
# Adds the shared key (which can correspond to the client_secret) as an oct key so it can be
|
||||
# used for HMAC signatures.
|
||||
logger.debug(log_prompt.format('Add key'))
|
||||
jwks_keys.add({'key': smart_bytes(shared_key), 'kty': 'oct'})
|
||||
logger.debug(log_prompt.format('End'))
|
||||
return jwks_keys
|
||||
|
||||
|
||||
def _validate_claims(id_token, nonce=None, validate_nonce=True):
|
||||
""" Validates the claims embedded in the JSON Web Token. """
|
||||
log_prompt = "Validate claims: {}"
|
||||
logger.debug(log_prompt.format('Start'))
|
||||
|
||||
iss_parsed_url = urlparse(id_token['iss'])
|
||||
provider_parsed_url = urlparse(settings.AUTH_OPENID_PROVIDER_ENDPOINT)
|
||||
if iss_parsed_url.netloc != provider_parsed_url.netloc:
|
||||
logger.debug(log_prompt.format('Invalid issuer'))
|
||||
raise SuspiciousOperation('Invalid issuer')
|
||||
|
||||
if isinstance(id_token['aud'], str):
|
||||
id_token['aud'] = [id_token['aud']]
|
||||
|
||||
if settings.AUTH_OPENID_CLIENT_ID not in id_token['aud']:
|
||||
logger.debug(log_prompt.format('Invalid audience'))
|
||||
raise SuspiciousOperation('Invalid audience')
|
||||
|
||||
if len(id_token['aud']) > 1 and 'azp' not in id_token:
|
||||
logger.debug(log_prompt.format('Incorrect id_token: azp'))
|
||||
raise SuspiciousOperation('Incorrect id_token: azp')
|
||||
|
||||
if 'azp' in id_token and id_token['azp'] != settings.AUTH_OPENID_CLIENT_ID:
|
||||
raise SuspiciousOperation('Incorrect id_token: azp')
|
||||
|
||||
utc_timestamp = timegm(dt.datetime.utcnow().utctimetuple())
|
||||
if utc_timestamp > id_token['exp']:
|
||||
logger.debug(log_prompt.format('Signature has expired'))
|
||||
raise SuspiciousOperation('Signature has expired')
|
||||
|
||||
if 'nbf' in id_token and utc_timestamp < id_token['nbf']:
|
||||
logger.debug(log_prompt.format('Incorrect id_token: nbf'))
|
||||
raise SuspiciousOperation('Incorrect id_token: nbf')
|
||||
|
||||
# Verifies that the token was issued in the allowed timeframe.
|
||||
if utc_timestamp > id_token['iat'] + settings.AUTH_OPENID_ID_TOKEN_MAX_AGE:
|
||||
logger.debug(log_prompt.format('Incorrect id_token: iat'))
|
||||
raise SuspiciousOperation('Incorrect id_token: iat')
|
||||
|
||||
# Validate the nonce to ensure the request was not modified if applicable.
|
||||
id_token_nonce = id_token.get('nonce', None)
|
||||
if validate_nonce and settings.AUTH_OPENID_USE_NONCE and id_token_nonce != nonce:
|
||||
logger.debug(log_prompt.format('Incorrect id_token: nonce'))
|
||||
raise SuspiciousOperation('Incorrect id_token: nonce')
|
||||
|
||||
logger.debug(log_prompt.format('End'))
|
||||
|
||||
|
||||
def build_absolute_uri(request, path=None):
|
||||
"""
|
||||
Build absolute redirect uri
|
||||
"""
|
||||
if path is None:
|
||||
path = '/'
|
||||
|
||||
if settings.BASE_SITE_URL:
|
||||
redirect_uri = urljoin(settings.BASE_SITE_URL, path)
|
||||
else:
|
||||
redirect_uri = request.build_absolute_uri(path)
|
||||
return redirect_uri
|
||||
222
apps/authentication/backends/oidc/views.py
Normal file
222
apps/authentication/backends/oidc/views.py
Normal file
@@ -0,0 +1,222 @@
|
||||
"""
|
||||
OpenID Connect relying party (RP) views
|
||||
=======================================
|
||||
|
||||
This modules defines views allowing to start the authorization and authentication process in
|
||||
order to authenticate a specific user. The most important views are: the "login" allowing to
|
||||
authenticate the users using the OP and get an authorizartion code, the callback view allowing
|
||||
to retrieve a valid token for the considered user and the logout view.
|
||||
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import auth
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.http import HttpResponseRedirect, QueryDict
|
||||
from django.urls import reverse
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.http import is_safe_url, urlencode
|
||||
from django.views.generic import View
|
||||
|
||||
from .utils import get_logger, build_absolute_uri
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class OIDCAuthRequestView(View):
|
||||
""" Allows to start the authorization flow in order to authenticate the end-user.
|
||||
|
||||
This view acts as the main endpoint to trigger the authentication process involving the OIDC
|
||||
provider (OP). It prepares an authentication request that will be sent to the authorization
|
||||
server in order to authenticate the end-user.
|
||||
|
||||
"""
|
||||
|
||||
http_method_names = ['get', ]
|
||||
|
||||
def get(self, request):
|
||||
""" Processes GET requests. """
|
||||
|
||||
log_prompt = "Process GET requests [OIDCAuthRequestView]: {}"
|
||||
logger.debug(log_prompt.format('Start'))
|
||||
|
||||
# Defines common parameters used to bootstrap the authentication request.
|
||||
logger.debug(log_prompt.format('Construct request params'))
|
||||
authentication_request_params = request.GET.dict()
|
||||
authentication_request_params.update({
|
||||
'scope': settings.AUTH_OPENID_SCOPES,
|
||||
'response_type': 'code',
|
||||
'client_id': settings.AUTH_OPENID_CLIENT_ID,
|
||||
'redirect_uri': build_absolute_uri(
|
||||
request, path=reverse(settings.AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME)
|
||||
)
|
||||
})
|
||||
|
||||
# States should be used! They are recommended in order to maintain state between the
|
||||
# authentication request and the callback.
|
||||
if settings.AUTH_OPENID_USE_STATE:
|
||||
logger.debug(log_prompt.format('Use state'))
|
||||
state = get_random_string(settings.AUTH_OPENID_STATE_LENGTH)
|
||||
authentication_request_params.update({'state': state})
|
||||
request.session['oidc_auth_state'] = state
|
||||
|
||||
# Nonces should be used too! In that case the generated nonce is stored both in the
|
||||
# authentication request parameters and in the user's session.
|
||||
if settings.AUTH_OPENID_USE_NONCE:
|
||||
logger.debug(log_prompt.format('Use nonce'))
|
||||
nonce = get_random_string(settings.AUTH_OPENID_NONCE_LENGTH)
|
||||
authentication_request_params.update({'nonce': nonce, })
|
||||
request.session['oidc_auth_nonce'] = nonce
|
||||
|
||||
# Stores the "next" URL in the session if applicable.
|
||||
logger.debug(log_prompt.format('Stores next url in the session'))
|
||||
next_url = request.GET.get('next')
|
||||
request.session['oidc_auth_next_url'] = next_url \
|
||||
if is_safe_url(url=next_url, allowed_hosts=(request.get_host(), )) else None
|
||||
|
||||
# Redirects the user to authorization endpoint.
|
||||
logger.debug(log_prompt.format('Construct redirect url'))
|
||||
query = urlencode(authentication_request_params)
|
||||
redirect_url = '{url}?{query}'.format(
|
||||
url=settings.AUTH_OPENID_PROVIDER_AUTHORIZATION_ENDPOINT, query=query)
|
||||
|
||||
logger.debug(log_prompt.format('Redirect'))
|
||||
return HttpResponseRedirect(redirect_url)
|
||||
|
||||
|
||||
class OIDCAuthCallbackView(View):
|
||||
""" Allows to complete the authentication process.
|
||||
|
||||
This view acts as the main endpoint to complete the authentication process involving the OIDC
|
||||
provider (OP). It checks the request sent by the OIDC provider in order to determine whether the
|
||||
considered was successfully authentified or not and authenticates the user at the current
|
||||
application level if applicable.
|
||||
|
||||
"""
|
||||
|
||||
http_method_names = ['get', ]
|
||||
|
||||
def get(self, request):
|
||||
""" Processes GET requests. """
|
||||
log_prompt = "Process GET requests [OIDCAuthCallbackView]: {}"
|
||||
logger.debug(log_prompt.format('Start'))
|
||||
callback_params = request.GET
|
||||
|
||||
# Retrieve the state value that was previously generated. No state means that we cannot
|
||||
# authenticate the user (so a failure should be returned).
|
||||
state = request.session.get('oidc_auth_state', None)
|
||||
|
||||
# Retrieve the nonce that was previously generated and remove it from the current session.
|
||||
# If no nonce is available (while the USE_NONCE setting is set to True) this means that the
|
||||
# authentication cannot be performed and so we have redirect the user to a failure URL.
|
||||
nonce = request.session.pop('oidc_auth_nonce', None)
|
||||
|
||||
# NOTE: a redirect to the failure page should be return if some required GET parameters are
|
||||
# missing or if no state can be retrieved from the current session.
|
||||
|
||||
if (
|
||||
((nonce and settings.AUTH_OPENID_USE_NONCE) or not settings.AUTH_OPENID_USE_NONCE)
|
||||
and
|
||||
(
|
||||
(state and settings.AUTH_OPENID_USE_STATE and 'state' in callback_params)
|
||||
or
|
||||
(not settings.AUTH_OPENID_USE_STATE)
|
||||
)
|
||||
and
|
||||
('code' in callback_params)
|
||||
):
|
||||
# Ensures that the passed state values is the same as the one that was previously
|
||||
# generated when forging the authorization request. This is necessary to mitigate
|
||||
# Cross-Site Request Forgery (CSRF, XSRF).
|
||||
if settings.AUTH_OPENID_USE_STATE and callback_params['state'] != state:
|
||||
logger.debug(log_prompt.format('Invalid OpenID Connect callback state value'))
|
||||
raise SuspiciousOperation('Invalid OpenID Connect callback state value')
|
||||
|
||||
# Authenticates the end-user.
|
||||
next_url = request.session.get('oidc_auth_next_url', None)
|
||||
logger.debug(log_prompt.format('Process authenticate'))
|
||||
user = auth.authenticate(nonce=nonce, request=request)
|
||||
if user and user.is_valid:
|
||||
logger.debug(log_prompt.format('Login: {}'.format(user)))
|
||||
auth.login(self.request, user)
|
||||
# Stores an expiration timestamp in the user's session. This value will be used if
|
||||
# the project is configured to periodically refresh user's token.
|
||||
self.request.session['oidc_auth_id_token_exp_timestamp'] = \
|
||||
time.time() + settings.AUTH_OPENID_ID_TOKEN_MAX_AGE
|
||||
# Stores the "session_state" value that can be passed by the OpenID Connect provider
|
||||
# in order to maintain a consistent session state across the OP and the related
|
||||
# relying parties (RP).
|
||||
self.request.session['oidc_auth_session_state'] = \
|
||||
callback_params.get('session_state', None)
|
||||
|
||||
logger.debug(log_prompt.format('Redirect'))
|
||||
return HttpResponseRedirect(
|
||||
next_url or settings.AUTH_OPENID_AUTHENTICATION_REDIRECT_URI
|
||||
)
|
||||
|
||||
if 'error' in callback_params:
|
||||
logger.debug(
|
||||
log_prompt.format('Error in callback params: {}'.format(callback_params['error']))
|
||||
)
|
||||
# If we receive an error in the callback GET parameters, this means that the
|
||||
# authentication could not be performed at the OP level. In that case we have to logout
|
||||
# the current user because we could've obtained this error after a prompt=none hit on
|
||||
# OpenID Connect Provider authenticate endpoint.
|
||||
logger.debug(log_prompt.format('Logout'))
|
||||
auth.logout(request)
|
||||
|
||||
logger.debug(log_prompt.format('Redirect'))
|
||||
return HttpResponseRedirect(settings.AUTH_OPENID_AUTHENTICATION_FAILURE_REDIRECT_URI)
|
||||
|
||||
|
||||
class OIDCEndSessionView(View):
|
||||
""" Allows to end the session of any user authenticated using OpenID Connect.
|
||||
|
||||
This view acts as the main endpoint to end the session of an end-user that was authenticated
|
||||
using the OIDC provider (OP). It calls the "end-session" endpoint provided by the provider if
|
||||
applicable.
|
||||
|
||||
"""
|
||||
|
||||
http_method_names = ['get', 'post', ]
|
||||
|
||||
def get(self, request):
|
||||
""" Processes GET requests. """
|
||||
log_prompt = "Process GET requests [OIDCEndSessionView]: {}"
|
||||
logger.debug(log_prompt.format('Start'))
|
||||
return self.post(request)
|
||||
|
||||
def post(self, request):
|
||||
""" Processes POST requests. """
|
||||
log_prompt = "Process POST requests [OIDCEndSessionView]: {}"
|
||||
logger.debug(log_prompt.format('Start'))
|
||||
|
||||
logout_url = settings.LOGOUT_REDIRECT_URL or '/'
|
||||
|
||||
# Log out the current user.
|
||||
if request.user.is_authenticated:
|
||||
logger.debug(log_prompt.format('Current user is authenticated'))
|
||||
try:
|
||||
logout_url = self.provider_end_session_url \
|
||||
if settings.AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT else logout_url
|
||||
except KeyError: # pragma: no cover
|
||||
logout_url = logout_url
|
||||
logger.debug(log_prompt.format('Log out the current user: {}'.format(request.user)))
|
||||
auth.logout(request)
|
||||
|
||||
# Redirects the user to the appropriate URL.
|
||||
logger.debug(log_prompt.format('Redirect'))
|
||||
return HttpResponseRedirect(logout_url)
|
||||
|
||||
@property
|
||||
def provider_end_session_url(self):
|
||||
""" Returns the end-session URL. """
|
||||
q = QueryDict(mutable=True)
|
||||
q[settings.AUTH_OPENID_PROVIDER_END_SESSION_REDIRECT_URI_PARAMETER] = \
|
||||
build_absolute_uri(self.request, path=settings.LOGOUT_REDIRECT_URL or '/')
|
||||
q[settings.AUTH_OPENID_PROVIDER_END_SESSION_ID_TOKEN_PARAMETER] = \
|
||||
self.request.session['oidc_auth_id_token']
|
||||
return '{}?{}'.format(settings.AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT, q.urlencode())
|
||||
@@ -1,4 +0,0 @@
|
||||
"""
|
||||
使用下面的工程,进行jumpserver 的 oidc 认证
|
||||
https://github.com/BaiJiangJie/jumpserver-django-oidc-rp
|
||||
"""
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user