├── .gitignore
├── README.md
├── base
├── __init__.py
├── admin.py
├── apps.py
├── cas_callbacks.py
├── ldap.py
├── migrations
│ ├── 0001_initial.py
│ └── __init__.py
├── models.py
├── tests.py
└── views.py
├── casdemo
├── __init__.py
├── auth.py
├── settings.py
├── templates
│ ├── base.html
│ ├── dingding
│ │ ├── config.html
│ │ ├── index.html
│ │ └── index_browser.html
│ ├── index.html
│ ├── login.html
│ ├── login_dingding.html
│ └── security
│ │ └── includes
│ │ └── admin_acl_list.html
├── urls.py
├── views.py
└── wsgi.py
├── db.sqlite3
├── dingding
├── __init__.py
├── admin.py
├── apps.py
├── cas_callbacks.py
├── client.py
├── middleware.py
├── migrations
│ ├── 0001_initial.py
│ └── __init__.py
├── models.py
├── templatetags
│ ├── __init__.py
│ └── dingding.py
├── tests.py
├── urls.py
└── views.py
├── manage.py
├── requirements.txt
└── security
├── __init__.py
├── admin.py
├── apps.py
├── cas_callbacks.py
├── migrations
├── 0001_initial.py
└── __init__.py
├── models.py
├── tests.py
├── urls.py
├── utils.py
└── views.py
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | *.pyc
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # cas demo django server
2 |
3 | ## cas server
4 |
5 | * edit hosts ```127.0.0.1 www.casdemo.com```
6 | * execute ```python manage.py runserver 0.0.0.0:8000```
7 | * `cas_callbacks.py` return user defined attributes
8 | * `admin/admin` for admin page, test1/test1 (~ test5/test5) for normal user
9 |
10 | ## ldap auth
11 |
12 | * check out at `casdemo.auth.ActiveDirectoryAuthenticationBackEnd`
13 |
14 |
15 | ## dingtalk
16 |
17 | * `client.py` helper functions for dingtalk api
18 | * `templates/dingding/` auto login for integration with dingding
19 |
--------------------------------------------------------------------------------
/base/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cangelzz/cas-demo-django-server/18774b75fee965048d0ebacdd35370f2286aabe1/base/__init__.py
--------------------------------------------------------------------------------
/base/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
3 | from django.contrib.auth.models import User
4 | from dingding.models import DingUser
5 | from security.models import ACL
6 | from django.contrib.admin import SimpleListFilter, FieldListFilter
7 | from django import forms
8 |
9 | from base.models import *
10 |
11 |
12 |
13 | class BaseAdmin(admin.ModelAdmin):
14 | actions_on_top = True
15 | actions_on_bottom = True
16 |
17 |
18 | class RootOUFilter(SimpleListFilter):
19 | title = "大部门"
20 | parameter_name = "root"
21 |
22 | def lookups(self, request, model_admin):
23 | return OU.objects.filter(parent__name="Root-Inc").values_list("id", "name")
24 |
25 | def queryset(self, request, queryset):
26 | if self.value():
27 | return queryset.filter(root_ou__id=self.value())
28 | else:
29 | return queryset
30 |
31 |
32 | class ActiveProfileListFilter(admin.SimpleListFilter):
33 | title = "在职状态"
34 | parameter_name = "active"
35 |
36 | def lookups(self, request, model_admin):
37 | return (
38 | (1, "在职"),
39 | (0, "离职")
40 | )
41 |
42 | def queryset(self, request, queryset):
43 | if self.value() == "1":
44 | return queryset.filter(user__is_active=True)
45 | if self.value() == "0":
46 | return queryset.filter(user__is_active=False)
47 |
48 |
49 | class ProfileAdmin(BaseAdmin):
50 | model = Profile
51 |
52 | def active(self):
53 | return self.user.is_active
54 |
55 | active.boolean = True
56 | active.short_description = "有效"
57 |
58 | list_display = ["user", "full_name", "display_name", "jobnumber", active, "department", "manager", "root_ou", "ou",
59 | "extNumber", "mobile"]
60 | search_fields = ["display_name", "xingming", "user__username", "jobnumber", "ou__name", "department", "extNumber",
61 | "mobile"]
62 | list_filter = [ActiveProfileListFilter, "department", RootOUFilter, ("manager", admin.RelatedOnlyFieldListFilter)]
63 | ordering = ["jobnumber"]
64 |
65 |
66 | class DepartmentAdmin(admin.ModelAdmin):
67 | pass
68 |
69 |
70 | class OUAdmin(BaseAdmin):
71 | list_display = ["name", "parent"]
72 |
73 |
74 | # Re-register UserAdmin
75 | admin.site.site_header = "Backend User Center"
76 | admin.site.unregister(User)
77 | admin.site.register(Profile, ProfileAdmin)
78 | admin.site.register(Department, DepartmentAdmin)
79 | admin.site.register(OU, OUAdmin)
80 |
--------------------------------------------------------------------------------
/base/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class BaseConfig(AppConfig):
5 | name = 'base'
6 |
--------------------------------------------------------------------------------
/base/cas_callbacks.py:
--------------------------------------------------------------------------------
1 |
2 | def user_profile_attributes(user, service):
3 | """Return all available user name related fields and methods."""
4 | attributes = {}
5 | attributes['username'] = user.get_username()
6 | attributes['fullname'] = user.profile.fullname()
7 | attributes['name'] = user.profile.full_name
8 | return attributes
--------------------------------------------------------------------------------
/base/ldap.py:
--------------------------------------------------------------------------------
1 | import re
2 | from ldap3 import Server, Connection, SUBTREE, NTLM, BASE, LEVEL
3 | from ldap3.core.exceptions import LDAPBindError, LDAPAttributeError
4 | from ldap3.extend.microsoft import modifyPassword, unlockAccount
5 | import getpass
6 | from django.conf import settings
7 | import ssl
8 |
9 | USER_BASE_DN = "OU=Test,DC=Test,DC=com"
10 | USER_CATEGORY_DN = "CN=Person,CN=Schema,CN=Configuration,DC=Test,DC=com"
11 | OU_CATEGORY_DN = "CN=Organizational-Unit,CN=Schema,CN=Configuration,DC=test,DC=com"
12 |
13 |
14 | def guid2str(val):
15 | import uuid
16 | return uuid.UUID(bytes_le=val).hex
17 |
18 | class LdapServer(object):
19 | """
20 | 示例代码,根据实际情况进行改写
21 | """
22 | def __init__(self, **options):
23 | username = options.get("username", None)
24 | password = options.get("password", None)
25 | use_ssl = options.get("use_ssl", False)
26 | server_name = options.get("server", settings.AD_SERVER_NAME)
27 | if not username:
28 | username = input("Enter username: ").strip()
29 |
30 | if not password:
31 | password = getpass.getpass("Enter password for %s: " % username).strip()
32 |
33 | try:
34 | server = Server(settings.AD_SERVER_NAME_WRITE, use_ssl=use_ssl)
35 | conn = Connection(server, "%s\\%s" % (settings.AD_DOMAIN, username), password, auto_bind=True,
36 | authentication=NTLM)
37 | self.conn = conn.bound and conn or None
38 | except LDAPBindError:
39 | print("error, wrong password")
40 | self.conn = None
41 |
42 | def search_user(self, username):
43 | if self.conn is None:
44 | return None
45 |
46 | result = self._search(USER_BASE_DN, "(sAMAccountName=%s)" % username, SUBTREE, attributes=["sn", "givenName"])
47 | for user in result:
48 | user["username"] = username
49 | user["email"] = "%s@%s" % (username, settings.AD_EMAIL_HOST)
50 |
51 | return result
52 |
53 | def get_user(self, username, attributes=["sn", "givenName"], base_dn=USER_BASE_DN):
54 | if self.conn is None:
55 | return None
56 |
57 | result = self._search(base_dn, "(sAMAccountName=%s)" % username, SUBTREE, attributes=attributes)
58 | cnt = len(result)
59 | if not cnt:
60 | return None
61 |
62 | return result[0]
63 |
64 | def _search(self, dn, cond, scope, attributes):
65 | if self.conn is None:
66 | return None
67 |
68 | if self.conn.search(dn, cond, scope, attributes=attributes):
69 | result = []
70 | for entry in self.conn.entries:
71 | item = {}
72 | for attr in attributes:
73 | item[attr] = self._get(entry, attr)
74 | result.append(item)
75 |
76 | return result
77 |
78 | return []
79 |
80 | def _get(self, result, key):
81 | try:
82 | return getattr(result, key).value
83 | except LDAPAttributeError:
84 | return ""
85 | except:
86 | return ""
87 |
88 |
89 | class NoConnectionException(Exception):
90 | pass
91 |
92 |
93 | class FailedConnectionException(Exception):
94 | pass
--------------------------------------------------------------------------------
/base/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.10.7 on 2017-11-06 02:13
3 | from __future__ import unicode_literals
4 |
5 | from django.conf import settings
6 | from django.db import migrations, models
7 | import django.db.models.deletion
8 |
9 |
10 | class Migration(migrations.Migration):
11 |
12 | initial = True
13 |
14 | dependencies = [
15 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
16 | ]
17 |
18 | operations = [
19 | migrations.CreateModel(
20 | name='Department',
21 | fields=[
22 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
23 | ('name', models.CharField(max_length=50, unique=True, verbose_name='部门名称')),
24 | ],
25 | options={
26 | 'verbose_name_plural': '部门',
27 | 'verbose_name': '部门',
28 | },
29 | ),
30 | migrations.CreateModel(
31 | name='OU',
32 | fields=[
33 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
34 | ('guid', models.CharField(max_length=32, unique=True, verbose_name='GUID')),
35 | ('dn', models.CharField(max_length=255, unique=True, verbose_name='DN')),
36 | ('name', models.CharField(max_length=20, verbose_name='部门名称')),
37 | ('order', models.PositiveIntegerField(default=0, verbose_name='排序')),
38 | ('hidden', models.BooleanField(default=False, verbose_name='隐藏')),
39 | ('parent', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='base.OU', verbose_name='上级部门')),
40 | ],
41 | options={
42 | 'verbose_name_plural': 'AD部门',
43 | 'verbose_name': 'AD部门',
44 | 'ordering': ['order', 'name'],
45 | },
46 | ),
47 | migrations.CreateModel(
48 | name='Profile',
49 | fields=[
50 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
51 | ('display_name', models.CharField(blank=True, default='', max_length=50, null=True, verbose_name='全名')),
52 | ('full_name', models.CharField(blank=True, default='', max_length=50, null=True, verbose_name='姓名')),
53 | ('xing', models.SlugField(default='a', verbose_name='英文姓')),
54 | ('ming', models.SlugField(default='a', verbose_name='英文名')),
55 | ('xingming', models.SlugField(default='a', verbose_name='英文姓名')),
56 | ('jobnumber', models.CharField(blank=True, default='', max_length=6, verbose_name='工号')),
57 | ('extNumber', models.CharField(blank=True, default='', max_length=4, null=True, verbose_name='分机号')),
58 | ('mobile', models.CharField(blank=True, max_length=11, null=True, verbose_name='手机号')),
59 | ('department', models.CharField(blank=True, default='', max_length=50, null=True, verbose_name='部门')),
60 | ('dn', models.CharField(blank=True, max_length=255, null=True, verbose_name='DN')),
61 | ('order', models.PositiveIntegerField(default=99999, verbose_name='排序')),
62 | ('manager', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='base.Profile', verbose_name='上级经理')),
63 | ('ou', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='direct_profiles', to='base.OU', verbose_name='AD部门')),
64 | ('root_ou', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='all_profiles', to='base.OU', verbose_name='大部门')),
65 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='域账户')),
66 | ],
67 | options={
68 | 'verbose_name_plural': '用户信息',
69 | 'verbose_name': '用户信息',
70 | 'ordering': ['order', 'xingming'],
71 | },
72 | ),
73 | migrations.CreateModel(
74 | name='Tag',
75 | fields=[
76 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
77 | ('text', models.CharField(max_length=50, unique=True, verbose_name='标签')),
78 | ],
79 | options={
80 | 'verbose_name_plural': '用户标签',
81 | 'verbose_name': '用户标签',
82 | },
83 | ),
84 | ]
85 |
--------------------------------------------------------------------------------
/base/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cangelzz/cas-demo-django-server/18774b75fee965048d0ebacdd35370f2286aabe1/base/migrations/__init__.py
--------------------------------------------------------------------------------
/base/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.contrib.auth.models import User
3 | from django.db.models.signals import post_save
4 | from uuslug import slugify
5 |
6 |
7 | def myslugify(s):
8 | return slugify(s).replace("-", "")
9 |
10 |
11 | class Tag(models.Model):
12 | class Meta:
13 | verbose_name = "用户标签"
14 | verbose_name_plural = "用户标签"
15 |
16 | text = models.CharField(max_length=50, verbose_name="标签", unique=True)
17 |
18 | def __str__(self):
19 | return self.text
20 |
21 |
22 | class Profile(models.Model):
23 | class Meta:
24 | verbose_name = "用户信息"
25 | verbose_name_plural = "用户信息"
26 | ordering = ["order", "xingming"]
27 |
28 | user = models.OneToOneField(User, on_delete=models.CASCADE, verbose_name="域账户")
29 | display_name = models.CharField(max_length=50, blank=True, null=True, default="", verbose_name="全名")
30 | full_name = models.CharField(max_length=50, blank=True, null=True, default="", verbose_name="姓名")
31 | xing = models.SlugField(default="a", verbose_name="英文姓")
32 | ming = models.SlugField(default="a", verbose_name="英文名")
33 | xingming = models.SlugField(default="a", verbose_name="英文姓名")
34 | jobnumber = models.CharField(max_length=6, blank=True, default="", verbose_name="工号")
35 | extNumber = models.CharField(max_length=4, blank=True, null=True, default="", verbose_name="分机号")
36 | mobile = models.CharField(max_length=11, null=True, blank=True, verbose_name="手机号")
37 | department = models.CharField(max_length=50, blank=True, null=True, default="", verbose_name="部门")
38 | ou = models.ForeignKey('OU', null=True, on_delete=models.SET_NULL, verbose_name="AD部门",
39 | related_name="direct_profiles", blank=True)
40 | dn = models.CharField(max_length=255, verbose_name='DN', null=True, blank=True)
41 | manager = models.ForeignKey('self', null=True, blank=True, on_delete=models.SET_NULL, verbose_name="上级经理")
42 | root_ou = models.ForeignKey('OU', null=True, blank=True, on_delete=models.SET_NULL, verbose_name="大部门",
43 | related_name="all_profiles")
44 | order = models.PositiveIntegerField(default=99999, verbose_name="排序")
45 |
46 | def fullname(self):
47 | return self.user.first_name is not "" and "%s%s" % (
48 | self.user.last_name, self.user.first_name) or self.user.username
49 |
50 | def __str__(self):
51 | return self.fullname()
52 |
53 | def save(self, *args, **kwargs):
54 | user = self.user
55 | if not self.id:
56 | self.xing = myslugify(user.last_name)
57 | self.ming = myslugify(user.first_name)
58 | self.xingming = "%s%s" % (self.xing, self.ming)
59 | super(Profile, self).save(*args, **kwargs)
60 |
61 |
62 | def create_user_profile(sender, instance, created, **kwargs):
63 | if created:
64 | Profile.objects.create(user=instance)
65 |
66 |
67 | post_save.connect(create_user_profile, sender=User)
68 |
69 |
70 | class Department(models.Model):
71 | class Meta:
72 | verbose_name = "部门"
73 | verbose_name_plural = "部门"
74 |
75 | name = models.CharField(max_length=50, unique=True, verbose_name="部门名称")
76 |
77 | def __str__(self):
78 | return self.name
79 |
80 |
81 | class OU(models.Model):
82 | class Meta:
83 | verbose_name = "AD部门"
84 | verbose_name_plural = "AD部门"
85 | ordering = ["order", "name"]
86 |
87 | guid = models.CharField(max_length=32, unique=True, verbose_name="GUID")
88 | dn = models.CharField(max_length=255, unique=True, verbose_name="DN")
89 | name = models.CharField(max_length=20, verbose_name="部门名称")
90 | parent = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, verbose_name="上级部门")
91 | order = models.PositiveIntegerField(default=0, verbose_name="排序")
92 | hidden = models.BooleanField(default=False, verbose_name="隐藏")
93 |
94 | def __str__(self):
95 | return self.name
96 |
97 | def get_root(self):
98 | ou = self
99 | while ou.parent and ou.parent.name != "Root-Inc":
100 | ou = ou.parent
101 |
102 | return ou
103 |
104 | def all_profiles_active(self):
105 | return self.all_profiles.filter(user__is_active=True)
106 |
107 | def direct_profiles_active(self):
108 | return self.direct_profiles.filter(user__is_active=True)
109 |
--------------------------------------------------------------------------------
/base/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/base/views.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import render
2 |
3 | # Create your views here.
4 |
--------------------------------------------------------------------------------
/casdemo/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cangelzz/cas-demo-django-server/18774b75fee965048d0ebacdd35370f2286aabe1/casdemo/__init__.py
--------------------------------------------------------------------------------
/casdemo/auth.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from ldap3 import Server, Connection, NTLM, SUBTREE
3 | from ldap3.core.exceptions import LDAPBindError
4 | from base.ldap import USER_BASE_DN
5 | from django.contrib.auth.models import User
6 | from django.conf import settings
7 | from django.utils.timezone import now
8 | from django.contrib.auth.signals import user_logged_in, user_logged_out, user_login_failed
9 | from axes.decorators import get_ip
10 |
11 | logger = logging.getLogger(__name__)
12 |
13 |
14 | class ActiveDirectoryAuthenticationBackEnd:
15 | def __init__(self):
16 | pass
17 |
18 | def authenticate(self, username, password, request=None):
19 | request_info = request and "%s %s" % (request.path, get_ip(request)) or ""
20 | if not username or not password:
21 | logger.info("Log In Failure [Empty] %s %s" % (username, request_info))
22 | return None
23 | try:
24 | server = Server(settings.AD_SERVER_NAME, use_ssl=True)
25 | conn = Connection(server, "%s\\%s" % (settings.AD_DOMAIN, username), password, auto_bind=True,
26 | authentication=NTLM)
27 | user = conn.bound and self.get_or_create_user(username, conn) or None
28 | if user is not None:
29 | pass
30 | else:
31 | logger.info("Log In Failure [NOTFOUND] %s %s" % (username, request_info))
32 | return user
33 | except LDAPBindError:
34 | logger.info("Log In Failure [LDAP] %s %s" % (username, request_info))
35 | return None
36 |
37 |
38 | def get_or_create_user(self, username, conn=None):
39 | try:
40 | user = User.objects.get(username=username)
41 | except User.DoesNotExist:
42 | if conn is None:
43 | return None
44 |
45 | if conn.search(USER_BASE_DN, "(sAMAccountName=%s)" % username, SUBTREE, attributes=["sn", "givenName"]):
46 | result = conn.response[0]["attributes"]
47 | user = User(username=username, password="testtest")
48 | user.first_name = result["givenName"]
49 | user.last_name = result["sn"]
50 | user.email = "%s@%s" % (username, settings.AD_EMAIL_HOST)
51 | user.save()
52 | else:
53 | logger.warning("user not found in AD")
54 | return None
55 | return user
56 |
57 | def get_user(self, user_id):
58 | try:
59 | return User.objects.get(pk=user_id)
60 | except User.DoesNotExist:
61 | return None
62 |
63 |
64 | def post_logged_in(sender, request, user, **kwargs):
65 | request_info = "%s %s" % (request.path, get_ip(request))
66 | logger.info("Log In Success %s %s" % (user.username, request_info))
67 |
68 |
69 | def post_logged_out(sender, request, user, **kwargs):
70 | request_info = "%s %s" % (request.path, get_ip(request))
71 | logger.info("Log Out %s %s" % (user and user.username or "none", request_info))
72 |
73 |
74 | def post_login_failed(sender, request, credentials, **kwargs):
75 | request_info = "%s %s" % (request.path, get_ip(request))
76 | logger.info("Signal Log In Failure %s %s" % (credentials.get("username", "-"), request_info))
77 |
78 |
79 | user_logged_in.connect(post_logged_in)
80 | user_logged_out.connect(post_logged_out)
81 | user_login_failed.connect(post_login_failed)
82 |
--------------------------------------------------------------------------------
/casdemo/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for casdemo project.
3 |
4 | Generated by 'django-admin startproject' using Django 1.10.7.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/1.10/topics/settings/
8 |
9 | For the full list of settings and their values, see
10 | https://docs.djangoproject.com/en/1.10/ref/settings/
11 | """
12 |
13 | import os
14 |
15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
17 |
18 |
19 | # Quick-start development settings - unsuitable for production
20 | # See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
21 |
22 | # SECURITY WARNING: keep the secret key used in production secret!
23 | SECRET_KEY = 'g))z$34j34jh3483h+da897sd23jk9sdusd0fs334ok0=j55eshn'
24 |
25 | # SECURITY WARNING: don't run with debug turned on in production!
26 | DEBUG = True
27 |
28 | ALLOWED_HOSTS = ["*"]
29 |
30 |
31 | # Application definition
32 |
33 | INSTALLED_APPS = [
34 | 'django.contrib.admin',
35 | 'django.contrib.auth',
36 | 'django.contrib.contenttypes',
37 | 'django.contrib.sessions',
38 | 'django.contrib.messages',
39 | 'django.contrib.staticfiles',
40 | 'mama_cas',
41 | 'base',
42 | 'dingding',
43 | 'security'
44 | ]
45 |
46 | MIDDLEWARE = [
47 | 'django.middleware.security.SecurityMiddleware',
48 | 'django.contrib.sessions.middleware.SessionMiddleware',
49 | 'django.middleware.common.CommonMiddleware',
50 | 'django.middleware.csrf.CsrfViewMiddleware',
51 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
52 | 'django.contrib.messages.middleware.MessageMiddleware',
53 | 'dingding.middleware.dingding_client_ua_middleware'
54 | ]
55 |
56 | ROOT_URLCONF = 'casdemo.urls'
57 |
58 | TEMPLATES = [
59 | {
60 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
61 | 'DIRS': [
62 | os.path.join(BASE_DIR, "casdemo", "templates")
63 | ],
64 | 'APP_DIRS': True,
65 | 'OPTIONS': {
66 | 'context_processors': [
67 | 'django.template.context_processors.debug',
68 | 'django.template.context_processors.request',
69 | 'django.contrib.auth.context_processors.auth',
70 | 'django.contrib.messages.context_processors.messages',
71 | ],
72 | },
73 | },
74 | ]
75 |
76 | WSGI_APPLICATION = 'casdemo.wsgi.application'
77 |
78 |
79 | # Database
80 | # https://docs.djangoproject.com/en/1.10/ref/settings/#databases
81 |
82 | DATABASES = {
83 | 'default': {
84 | 'ENGINE': 'django.db.backends.sqlite3',
85 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
86 | }
87 | }
88 |
89 |
90 | AUTHENTICATION_BACKENDS = [
91 | # 加入LDAP认证
92 | #'casdemo.auth.ActiveDirectoryAuthenticationBackEnd',
93 | 'django.contrib.auth.backends.ModelBackend'
94 | ]
95 |
96 |
97 | # Internationalization
98 | # https://docs.djangoproject.com/en/1.10/topics/i18n/
99 |
100 | LANGUAGE_CODE = 'zh-hans'
101 |
102 | TIME_ZONE = 'Asia/Shanghai'
103 |
104 | USE_I18N = True
105 |
106 | USE_L10N = True
107 |
108 | USE_TZ = True
109 |
110 |
111 | # Static files (CSS, JavaScript, Images)
112 | # https://docs.djangoproject.com/en/1.10/howto/static-files/
113 |
114 | STATIC_URL = '/static/'
115 |
116 | # 中央认证
117 | MAMA_CAS_SERVICES = [
118 | {
119 | 'SERVICE': '.*',
120 | 'CALLBACKS': [
121 | 'base.cas_callbacks.user_profile_attributes',
122 | 'dingding.cas_callbacks.user_ding_attributes',
123 | 'security.cas_callbacks.user_security_attributes',
124 | ],
125 | 'LOGOUT_ALLOW': True,
126 | 'LOGOUT_URL': '',
127 | }
128 | ]
129 |
--------------------------------------------------------------------------------
/casdemo/templates/base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {% block title %}{% endblock %}
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | {% if request.from_device == "pc" %}
14 |
15 | {% elif request.from_device == "mobile" %}
16 |
17 | {% endif %}
18 | {% block head %}
19 | {% endblock %}
20 |
21 |
22 | {% block nav %}
23 | {% endblock %}
24 | {% block main %}
25 | {% endblock %}
26 |
27 |
28 |
--------------------------------------------------------------------------------
/casdemo/templates/dingding/config.html:
--------------------------------------------------------------------------------
1 |
26 |
--------------------------------------------------------------------------------
/casdemo/templates/dingding/index.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% block main %}
3 |
4 | 自动登录中,请稍候 ...
5 |
6 |
7 | {% if from == "pc" %}
8 |
45 | {% elif from == "mobile" %}
46 |
82 | {% endif %}
83 | {% endblock %}
84 |
--------------------------------------------------------------------------------
/casdemo/templates/dingding/index_browser.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% block head %}
3 | {% endblock %}
4 | {% block main %}
5 |
6 |
尚未绑定钉钉帐号,请从企业应用中打开
7 |
8 | {% endblock %}
--------------------------------------------------------------------------------
/casdemo/templates/index.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% block title %}首页{% endblock %}
3 | {% block main %}
4 |
5 |
6 |
7 | {% if user.is_authenticated %}
8 |
欢迎 {{ user.username }}, 从 这里 登出。
9 | {% else %}
10 | 您好,请从
这里 登录。
11 | {% endif %}
12 |
13 | {% if user.is_authenticated %}
14 | 您拥有的角色:
15 |
16 | {% for role in roles %}
17 | - {{ role.application.description }} - {{ role.description }}
18 | {% endfor %}
19 |
20 |
21 | 您拥有的权限:
22 |
23 | {% for permission in permissions %}
24 | - {{ permission.application.description }} - {{ permission.description }}
25 | {% endfor %}
26 |
27 |
28 | {% endif %}
29 |
30 |
31 |
32 |
33 |
34 | {% endblock %}
--------------------------------------------------------------------------------
/casdemo/templates/login.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% block title %}登录{% endblock %}
3 | {% block main %}
4 |
48 | {% endblock %}
49 | {% block footer %}{% endblock %}
50 |
--------------------------------------------------------------------------------
/casdemo/templates/login_dingding.html:
--------------------------------------------------------------------------------
1 | {% extends "login.html" %}
2 | {% load dingding %}
3 | {% block head %}
4 | {% if request.dingding %}
5 | {% ding_config request %}
6 |
7 |
57 | {% endif %}
58 | {% endblock %}
59 | {% block login_hint %}
60 | {{ form_title }}
61 | {% endblock %}
62 |
--------------------------------------------------------------------------------
/casdemo/templates/security/includes/admin_acl_list.html:
--------------------------------------------------------------------------------
1 |
2 | {% for item in items %}
3 |
4 | {{ item.application.get_display_name }} |
5 | {{ item.get_display_name }} |
6 |
7 | {% endfor %}
8 |
--------------------------------------------------------------------------------
/casdemo/urls.py:
--------------------------------------------------------------------------------
1 | """casdemo URL Configuration
2 |
3 | The `urlpatterns` list routes URLs to views. For more information please see:
4 | https://docs.djangoproject.com/en/1.10/topics/http/urls/
5 | Examples:
6 | Function views
7 | 1. Add an import: from my_app import views
8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
9 | Class-based views
10 | 1. Add an import: from other_app.views import Home
11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
12 | Including another URLconf
13 | 1. Import the include() function: from django.conf.urls import url, include
14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
15 | """
16 | from django.conf.urls import url, include
17 | from django.contrib import admin
18 | from . import views
19 |
20 | urlpatterns = [
21 | url(r'^admin/', admin.site.urls),
22 | url(r"^cas/", include("mama_cas.urls")),
23 | url(r"^security/", include("security.urls", namespace="security")),
24 | url(r"^dingding/", include("dingding.urls", namespace="dingding")),
25 | url(r"^login/$", views.login_user, name="login"),
26 | url(r"^logout/$", views.logout_user, name="logout"),
27 | url(r"^$", views.index, name="index")
28 |
29 | ]
30 |
--------------------------------------------------------------------------------
/casdemo/views.py:
--------------------------------------------------------------------------------
1 | from dingding.client import ClientWrapper
2 | from django.contrib.auth import authenticate, login, logout
3 | from django.template.response import TemplateResponse
4 | from django.contrib import messages
5 | from django.conf import settings
6 | from django.shortcuts import *
7 |
8 |
9 | def index(request):
10 | if request.user.is_authenticated():
11 | from security.utils import get_roles, get_permissions
12 | roles = get_roles(request.user)
13 | permissions = get_permissions(request.user)
14 | return TemplateResponse(request, "index.html", {"roles": roles, "permissions": permissions})
15 |
16 | return TemplateResponse(request, "index.html", {})
17 |
18 | def login_user(request):
19 | if request.POST:
20 | username = request.POST.get("username", "").rsplit("\\")[-1].split("@")[0]
21 | password = request.POST.get("password", "")
22 | next = request.POST.get("next", "/")
23 | if username and password:
24 | user = authenticate(username=username, password=password, request=request)
25 | else:
26 | user = None
27 | if user is not None:
28 | login(request, user)
29 | response = redirect(next)
30 | return response
31 |
32 | messages.error(request, "账号密码有误,请重试")
33 |
34 | #For dingding auto logging
35 | next = request.GET.get("next", "/")
36 | if request.from_device in ["pc", "mobile"]:
37 | from_device = request.from_device
38 | client = ClientWrapper("portal")
39 | data = client.get_request_signature(request.build_absolute_uri())
40 | data["agent_id"] = client.corpinfo["config"][2]
41 | data["corp_id"] = client.corpinfo["config"][0]
42 | data["from"] = from_device
43 | data["next"] = next
44 | data["form_title"] = "正在尝试自动登录..."
45 | return TemplateResponse(request, "login_dingding.html", data)
46 |
47 | action = request.GET.get("action", "")
48 | data = {
49 | "next": next,
50 | "action": action,
51 | "ding_user_id": request.GET.get("ding_user_id")
52 | }
53 | if action == "bind":
54 | data["form_title"] = "请登录域帐号以绑定钉钉"
55 | else:
56 | data["form_title"] = "请登录域帐号"
57 |
58 | return TemplateResponse(request, "login.html", data)
59 |
60 |
61 | def logout_user(request):
62 | logout(request)
63 | request.session.pop("logged_ding_user_id", None)
64 | request.session.pop("verified", None)
65 | next = request.GET.get("next", "/")
66 | return redirect(next)
67 |
--------------------------------------------------------------------------------
/casdemo/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for casdemo project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "casdemo.settings")
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/db.sqlite3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cangelzz/cas-demo-django-server/18774b75fee965048d0ebacdd35370f2286aabe1/db.sqlite3
--------------------------------------------------------------------------------
/dingding/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cangelzz/cas-demo-django-server/18774b75fee965048d0ebacdd35370f2286aabe1/dingding/__init__.py
--------------------------------------------------------------------------------
/dingding/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from dingding.models import *
3 | from base.admin import BaseAdmin
4 | from django.utils.safestring import mark_safe
5 |
6 | class DingUserAdmin(BaseAdmin):
7 | def binded(self):
8 | return self.user_id and True or False
9 |
10 | ordering = ["aduser__username"]
11 | binded.boolean = True
12 | binded.short_description = "绑定状态"
13 | list_display = list(admin.ModelAdmin.list_display) + ["aduser", binded, "user_id"]
14 | search_fields = ["aduser__username"]
15 |
16 |
17 | class ApplicationInline(admin.StackedInline):
18 | model = Application
19 | can_delete = True
20 | extra = 0
21 |
22 |
23 | class CompanyAdmin(BaseAdmin):
24 | def apps(self):
25 | a = []
26 | for app in self.application_set.all():
27 | a.append("%d: %s: %s" % (app.id, app.name, app.agent_id))
28 | return mark_safe("
".join(a))
29 |
30 | list_display = ["name", "corp_id", "corp_secret", apps]
31 | inlines = [ApplicationInline]
32 |
33 | admin.site.register(DingUser, DingUserAdmin)
34 | admin.site.register(Company, CompanyAdmin)
35 |
--------------------------------------------------------------------------------
/dingding/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class DingdingConfig(AppConfig):
5 | name = 'dingding'
6 |
--------------------------------------------------------------------------------
/dingding/cas_callbacks.py:
--------------------------------------------------------------------------------
1 |
2 | def user_ding_attributes(user, service):
3 | """Return all available user name related fields and methods."""
4 | attributes = {}
5 | attributes['ding_user_id'] = user.dinguser.user_id
6 | attributes['mobile'] = user.dinguser.mobile or user.profile.mobile
7 | return attributes
--------------------------------------------------------------------------------
/dingding/client.py:
--------------------------------------------------------------------------------
1 | import requests
2 | from time import time
3 | import uuid
4 | import json
5 | import logging
6 | from hashlib import sha1
7 | from datetime import datetime
8 | from django.contrib.auth.models import User
9 | from .models import ChatGroup, Application
10 |
11 | logger = logging.getLogger(__name__)
12 |
13 | API_ADDR = "oapi.dingtalk.com"
14 |
15 |
16 | class ClientNotValidException(Exception):
17 | pass
18 |
19 |
20 | def http_get(url, params=None):
21 | try:
22 | result = requests.get(url, params)
23 | try:
24 | return True, result.json()
25 | except:
26 | return False, result.content
27 | except Exception as e:
28 | return False, e.message
29 |
30 |
31 | def http_post(url, data):
32 | headers = {
33 | "Content-Type": "application/json",
34 | "Accept-Charset": "utf-8"
35 | }
36 | try:
37 | result = requests.post(url, json.dumps(data), headers=headers)
38 | try:
39 | return True, result.json()
40 | except:
41 | return False, result.content
42 | except Exception as e:
43 | import traceback
44 | logger.error(traceback.format_exc())
45 | return False, e
46 |
47 |
48 | def token_required(func):
49 | def wrapper(self, *arg, **kwargs):
50 | if self.access_token is None:
51 | self.get_access_token()
52 | if int(time()) > self.token_expires:
53 | self.get_access_token()
54 | return func(self, *arg, **kwargs)
55 |
56 | return wrapper
57 |
58 |
59 | class Client:
60 | def __init__(self, CORP_INFO):
61 | self.corpinfo = CORP_INFO
62 | self.access_token = None
63 | self.token_expires = 0
64 | self.corpid, self.corpsecret, self.agentid = self.corpinfo["config"]
65 |
66 | def auth(self):
67 | is_success, token = self.get_access_token()
68 | return is_success
69 |
70 | def get_access_token(self):
71 | url = "https://%s/gettoken" % (API_ADDR)
72 | params = {"corpid": self.corpid, "corpsecret": self.corpsecret}
73 | is_success, result = http_get(url, params)
74 | if is_success:
75 | self.access_token = result.get("access_token", None)
76 | if self.access_token is None:
77 | raise ClientNotValidException()
78 |
79 | self.token_expires = int(time()) + 3600
80 | return True, self.access_token
81 |
82 | return False, result
83 |
84 | @token_required
85 | def get_jsapi_ticket(self):
86 | url = "https://%s/get_jsapi_ticket" % API_ADDR
87 | params = {"access_token": self.access_token, "type": "jsapi"}
88 | return http_get(url, params)
89 |
90 | def get_timestamp(self):
91 | return str(int(time()))
92 |
93 | def get_noncestr(self):
94 | return uuid.uuid4().hex
95 |
96 | def sign(self, jsapi_ticket, noncestr, timestamp, url):
97 | s = "jsapi_ticket=%s&noncestr=%s×tamp=%s&url=%s" % (jsapi_ticket, noncestr, timestamp, url)
98 | return sha1(s.encode("utf-8")).hexdigest()
99 |
100 | @token_required
101 | def get_request_signature(self, url):
102 | is_success, result = self.get_jsapi_ticket()
103 | if not is_success:
104 | return None
105 | ticket = result["ticket"]
106 | noncestr = self.get_noncestr()
107 | timestamp = self.get_timestamp()
108 | return {"signature": self.sign(ticket, noncestr, timestamp, url), "noncestr": noncestr,
109 | "timestamp": timestamp, "url": url, "ticket": ticket}
110 |
111 | @token_required
112 | def get_user(self, user_id):
113 | url = "https://%s/user/get" % API_ADDR
114 | params = {
115 | "access_token": self.access_token,
116 | "userid": user_id
117 | }
118 | return http_get(url, params)
119 |
120 | @token_required
121 | def get_user_info(self, code):
122 | url = "https://%s/user/getuserinfo" % API_ADDR
123 | params = {
124 | "access_token": self.access_token,
125 | "code": code
126 | }
127 | return http_get(url, params)
128 |
129 | @token_required
130 | def get_user_simple_list(self, department_id=1):
131 | url = "https://%s/user/simplelist?" % API_ADDR
132 | params = {
133 | "access_token": self.access_token,
134 | "department_id": department_id,
135 | }
136 | return http_get(url, params)
137 |
138 | @token_required
139 | def get_user_detail_list(self, department_id=1):
140 | url = "https://%s/user/list?" % API_ADDR
141 | params = {
142 | "access_token": self.access_token,
143 | "department_id": department_id,
144 | }
145 | return http_get(url, params)
146 |
147 | @token_required
148 | def get_department_list(self):
149 | url = "https://%s/department/list?access_token=%s" % (API_ADDR, self.access_token)
150 | return http_get(url)
151 |
152 | @token_required
153 | def get_user_all_list(self, usernames=[]):
154 | dept_success, result = self.get_department_list()
155 | if not dept_success:
156 | return dept_success, result
157 | users = []
158 | for department in result["department"]:
159 | print("getting department %s(%d) ..." % (department["name"], department["id"]))
160 | us_success, us = self.get_user_detail_list(department["id"])
161 | if usernames:
162 | for u in us["userlist"]:
163 | adname = u.get("extattr", {}).get("\u57df\u8d26\u53f7", "").split("\\")[-1]
164 | if adname in usernames:
165 | users.append(u)
166 | else:
167 | users.extend(us["userlist"])
168 |
169 | return True, users
170 |
171 |
172 | class ClientWrapper(Client):
173 | def __init__(self, app_name="portal"):
174 | app = Application.objects.get(name=app_name)
175 | super(ClientWrapper, self).__init__({"config": (app.company.corp_id, app.company.corp_secret, app.agent_id)})
176 |
177 |
178 | if __name__ == "__main__":
179 | pass
180 |
--------------------------------------------------------------------------------
/dingding/middleware.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | """
4 | win
5 | Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrom
6 | e/49.0.2623.110 Safari/537.36 dingtalk-win/1.0.0 nw(0.14.7) DingTalk(3.1.3-RC.0)
7 |
8 | android
9 | Mozilla/5.0 (Linux; Android 4.4.4; L55u Build/23.0.1.F.0.98) AppleWebKit/537.36
10 | (KHTML, like Gecko) Version/4.0 Chrome/33.0.0.0 Mobile Safari/537.36 AliApp(Ding
11 | Talk/3.1.0) com.alibaba.android.rimet/0 Channel/10002068 language/zh-CN
12 |
13 | mac
14 | Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like
15 | Gecko) Chrome/537.36 (@c26c0312e940221c424c2730ef72be2c69ac1b67) Safari/537.36 n
16 | w(0.14.7) DingTalk(3.1.0)
17 |
18 | iphone
19 | Mozilla/5.0 (iPhone; CPU iPhone OS 9_2_1 like Mac OS X) AppleWebKit/601.1.46 (KH
20 | TML, like Gecko) Mobile/13D15 AliApp(DingTalk/3.0.0) com.laiwang.DingTalk/177155
21 | 0 Channel/201200 language/zh-Hans
22 | """
23 |
24 | mobile_pattern = re.compile("mobile", re.I)
25 | idevice = re.compile(r"(iPhone|iPad|iPod)", re.I)
26 |
27 | def dingding_client_ua_middleware(get_response):
28 | def middleware(request):
29 | ua = request.META.get("HTTP_USER_AGENT", "")
30 | request.from_device = "browser"
31 | request.dingding = False
32 | request.mobile = mobile_pattern.search(ua) and True or False
33 | if ua.find("DingTalk") >= 0:
34 | request.dingding = True
35 | if ua.find("dingtalk-win") >= 0:
36 | request.from_device = "pc"
37 | elif ua.find("com.alibaba.android") >= 0 or idevice.search(ua):
38 | request.from_device = "mobile"
39 | else:
40 | request.from_device = "pc" #mac
41 |
42 | response = get_response(request)
43 | return response
44 | return middleware
45 |
--------------------------------------------------------------------------------
/dingding/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.10.7 on 2017-11-06 02:13
3 | from __future__ import unicode_literals
4 |
5 | from django.conf import settings
6 | import django.core.validators
7 | from django.db import migrations, models
8 | import django.db.models.deletion
9 | import re
10 |
11 |
12 | class Migration(migrations.Migration):
13 |
14 | initial = True
15 |
16 | dependencies = [
17 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
18 | ]
19 |
20 | operations = [
21 | migrations.CreateModel(
22 | name='Application',
23 | fields=[
24 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
25 | ('name', models.CharField(max_length=10, unique=True, validators=[django.core.validators.RegexValidator(re.compile('^[-a-zA-Z0-9_]+\\Z', 32), "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens.", 'invalid')], verbose_name='应用名称')),
26 | ('description', models.CharField(max_length=100, verbose_name='应用描述')),
27 | ('agent_id', models.IntegerField(verbose_name='Agent ID')),
28 | ],
29 | options={
30 | 'verbose_name_plural': '钉钉应用',
31 | 'verbose_name': '钉钉应用',
32 | },
33 | ),
34 | migrations.CreateModel(
35 | name='ChatGroup',
36 | fields=[
37 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
38 | ('name', models.CharField(db_index=True, max_length=20, unique=True, validators=[django.core.validators.RegexValidator(re.compile('^[-a-zA-Z0-9_]+\\Z', 32), "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens.", 'invalid')], verbose_name='群名称')),
39 | ('description', models.CharField(blank=True, default='', max_length=100, verbose_name='描述')),
40 | ('chat_id', models.CharField(blank=True, default='', max_length=100, verbose_name='群ID')),
41 | ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ownedchatgroups', to=settings.AUTH_USER_MODEL, verbose_name='群主')),
42 | ('users', models.ManyToManyField(related_name='chatgroups', to=settings.AUTH_USER_MODEL, verbose_name='群成员')),
43 | ],
44 | options={
45 | 'verbose_name_plural': '钉钉群',
46 | 'verbose_name': '钉钉群',
47 | },
48 | ),
49 | migrations.CreateModel(
50 | name='Company',
51 | fields=[
52 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
53 | ('name', models.CharField(max_length=10, unique=True, validators=[django.core.validators.RegexValidator(re.compile('^[-a-zA-Z0-9_]+\\Z', 32), "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens.", 'invalid')], verbose_name='企业名称')),
54 | ('description', models.CharField(blank=True, max_length=100, null=True, verbose_name='企业描述')),
55 | ('corp_id', models.CharField(max_length=32, verbose_name='Corp ID')),
56 | ('corp_secret', models.CharField(max_length=64, verbose_name='Corp Secret')),
57 | ],
58 | options={
59 | 'verbose_name_plural': '钉钉企业',
60 | 'verbose_name': '钉钉企业',
61 | },
62 | ),
63 | migrations.CreateModel(
64 | name='DingUser',
65 | fields=[
66 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
67 | ('name', models.CharField(blank=True, max_length=50, null=True, verbose_name='姓名')),
68 | ('avatar', models.URLField(blank=True, null=True, verbose_name='头像')),
69 | ('ding_id', models.CharField(blank=True, max_length=64, null=True, verbose_name='全局用户ID')),
70 | ('user_id', models.CharField(blank=True, max_length=64, null=True, verbose_name='企业用户ID')),
71 | ('mobile', models.CharField(blank=True, max_length=11, null=True, verbose_name='手机号')),
72 | ('aduser', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='域账户')),
73 | ],
74 | options={
75 | 'verbose_name_plural': '钉钉帐号',
76 | 'verbose_name': '钉钉帐号',
77 | },
78 | ),
79 | migrations.AddField(
80 | model_name='application',
81 | name='company',
82 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dingding.Company', verbose_name='企业'),
83 | ),
84 | migrations.AlterUniqueTogether(
85 | name='application',
86 | unique_together=set([('company', 'name'), ('company', 'agent_id')]),
87 | ),
88 | ]
89 |
--------------------------------------------------------------------------------
/dingding/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cangelzz/cas-demo-django-server/18774b75fee965048d0ebacdd35370f2286aabe1/dingding/migrations/__init__.py
--------------------------------------------------------------------------------
/dingding/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.db.models.signals import post_save
3 | from django.contrib.auth.models import User
4 | from django.core.validators import validate_slug
5 | from django.contrib.sites.shortcuts import get_current_site
6 |
7 |
8 | class DingUser(models.Model):
9 | class Meta:
10 | verbose_name = "钉钉帐号"
11 | verbose_name_plural = "钉钉帐号"
12 |
13 | aduser = models.OneToOneField(User, on_delete=models.CASCADE, verbose_name="域账户")
14 | name = models.CharField(max_length=50, null=True, blank=True, verbose_name="姓名")
15 | avatar = models.URLField(null=True, blank=True, verbose_name="头像")
16 | ding_id = models.CharField(max_length=64, null=True, blank=True, verbose_name="全局用户ID")
17 | user_id = models.CharField(max_length=64, null=True, blank=True, verbose_name="企业用户ID")
18 | mobile = models.CharField(max_length=11, null=True, blank=True, verbose_name="手机号")
19 |
20 | def __str__(self):
21 | return self.aduser.profile.fullname()
22 |
23 | def get_avatar(self):
24 | avatar = (self.avatar in ["", "undefined"] or self.avatar is None) and "/static/anonymouse.png" or self.avatar
25 | avatar = avatar.replace("http:", "")
26 | return avatar
27 |
28 | def get_absolute_avatar(self):
29 | avatar = (self.avatar in ["", "undefined"] or self.avatar is None) and "//%s/static/anonymouse.png" % get_current_site(None).domain or self.avatar
30 | avatar = avatar.replace("http:", "")
31 | return avatar
32 |
33 | @property
34 | def avatar_url(self):
35 | if self.avatar is None or self.avatar in ["", "undefined"]:
36 | return None
37 | else:
38 | return self.avatar.replace("http://", "https://")
39 |
40 |
41 | def create_ding_user(sender, instance, created, **kwargs):
42 | DingUser.objects.get_or_create(aduser=instance)
43 |
44 |
45 | post_save.connect(create_ding_user, sender=User)
46 |
47 |
48 | class ChatGroup(models.Model):
49 | class Meta:
50 | verbose_name = "钉钉群"
51 | verbose_name_plural = "钉钉群"
52 |
53 | owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name="ownedchatgroups", verbose_name="群主")
54 | name = models.CharField(max_length=20, validators=[validate_slug], unique=True, db_index=True, verbose_name="群名称")
55 | description = models.CharField(max_length=100, blank=True, default="", verbose_name="描述")
56 | chat_id = models.CharField(max_length=100, blank=True, default="", verbose_name="群ID")
57 | users = models.ManyToManyField(User, related_name="chatgroups", verbose_name="群成员")
58 |
59 | def __str__(self):
60 | return "%s %s %s %s" % (self.description, self.name, self.owner.profile.fullname(), self.chat_id)
61 |
62 |
63 | class Company(models.Model):
64 | class Meta:
65 | verbose_name = "钉钉企业"
66 | verbose_name_plural = "钉钉企业"
67 |
68 | name = models.CharField(max_length=10, unique=True, validators=[validate_slug], verbose_name="企业名称")
69 | description = models.CharField(max_length=100, null=True, blank=True, verbose_name="企业描述")
70 | corp_id = models.CharField(max_length=32, verbose_name="Corp ID")
71 | corp_secret = models.CharField(max_length=64, verbose_name="Corp Secret")
72 |
73 | def __str__(self):
74 | return self.name
75 |
76 |
77 | class Application(models.Model):
78 | class Meta:
79 | verbose_name = "钉钉应用"
80 | verbose_name_plural = "钉钉应用"
81 | unique_together = [("company", "name"), ("company", "agent_id")]
82 |
83 | company = models.ForeignKey(Company, verbose_name="企业")
84 | name = models.CharField(unique=True, max_length=10, validators=[validate_slug], verbose_name="应用名称")
85 | description = models.CharField(max_length=100, verbose_name="应用描述")
86 | agent_id = models.IntegerField(verbose_name="Agent ID")
87 |
88 | def __str__(self):
89 | return "%s: %s" % (self.company, self.name)
90 |
--------------------------------------------------------------------------------
/dingding/templatetags/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cangelzz/cas-demo-django-server/18774b75fee965048d0ebacdd35370f2286aabe1/dingding/templatetags/__init__.py
--------------------------------------------------------------------------------
/dingding/templatetags/dingding.py:
--------------------------------------------------------------------------------
1 | from django import template
2 | from dingding.client import ClientWrapper
3 |
4 | register = template.Library()
5 |
6 |
7 | @register.inclusion_tag("dingding/config.html")
8 | def ding_config(request):
9 | client = ClientWrapper("portal")
10 | url = request.build_absolute_uri()
11 | data = client.get_request_signature(url)
12 | data["agent_id"] = client.corpinfo["config"][2]
13 | data["corp_id"] = client.corpinfo["config"][0]
14 | return data
15 |
--------------------------------------------------------------------------------
/dingding/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/dingding/urls.py:
--------------------------------------------------------------------------------
1 | from . import views
2 | from django.conf.urls import url
3 |
4 | urlpatterns = [
5 | url(r"^login/$", views.login_user),
6 | ]
7 |
--------------------------------------------------------------------------------
/dingding/views.py:
--------------------------------------------------------------------------------
1 | import json
2 | from .models import DingUser, ChatGroup
3 | from .client import ClientWrapper
4 | from django.template.response import TemplateResponse
5 | from django.http.response import HttpResponse, JsonResponse, HttpResponseBadRequest, HttpResponseNotFound
6 | from django.contrib.auth import login, logout
7 | from django.shortcuts import redirect
8 | from django.contrib.auth.models import User
9 | from django.views.decorators.csrf import csrf_exempt
10 | import logging
11 | from django.core.mail import send_mail
12 | from datetime import datetime
13 | from django.conf import settings
14 | import os
15 | import re
16 | from django.shortcuts import get_object_or_404
17 | from django.urls import reverse
18 | from django.contrib.auth.decorators import login_required
19 |
20 | log = logging.getLogger(__name__)
21 |
22 |
23 | def index(request):
24 | if request.session.get("logged_ding_user_id"):
25 | return redirect("/")
26 |
27 | from_device = request.from_device # GET.get("from", None)
28 | if from_device is None:
29 | if not request.user.is_authenticated():
30 | return redirect("/login/?next=/dingding/")
31 |
32 | if not request.user.dinguser.user_id:
33 | return TemplateResponse(request, "dingding/index_browser.html", {})
34 |
35 | request.session["logged_ding_user_id"] = request.user.dinguser.user_id
36 | return redirect("/")
37 |
38 | client = ClientWrapper()
39 | data = client.get_request_signature(request.build_absolute_uri())
40 | data["agent_id"] = client.corpinfo["config"][2]
41 | data["corp_id"] = client.corpinfo["config"][0]
42 | data["from"] = from_device
43 |
44 | return TemplateResponse(request, "dingding/index.html", data)
45 |
46 |
47 | def login_user(request):
48 | code = request.GET.get("code")
49 | next = request.GET.get("next", "/")
50 | avatar = request.GET.get("avatar", "")
51 | nick = request.GET.get("nick", "")
52 | client = ClientWrapper()
53 | is_success, result = client.get_user_info(code)
54 | if is_success:
55 | user_id = result.get("userid")
56 | if not user_id:
57 | log.warning("userid is none from result: " + json.dumps(result, indent=2, ensure_ascii=False))
58 | return redirect("/")
59 | try:
60 | try:
61 | dinguser = DingUser.objects.get(user_id=user_id)
62 | except DingUser.MultipleObjectsReturned:
63 | log.warning("DingUser has multiple objects for user_id:" + user_id)
64 | dinguser = DingUser.objects.filter(user_id=user_id).first()
65 | if nick:
66 | dinguser.name = nick
67 | if avatar:
68 | dinguser.avatar = avatar
69 | dinguser.save()
70 |
71 | user = dinguser.aduser
72 | if request.user.is_authenticated and request.user.username != user.username:
73 | logout(request)
74 |
75 | login(request, user, "casdemo.auth.ActiveDirectoryAuthenticationBackEnd")
76 | request.session["logged_ding_user_id"] = user_id
77 |
78 | return redirect(next)
79 | except DingUser.DoesNotExist:
80 | return redirect("/login/?next=/dingding/&action=bind&ding_user_id=%s" % user_id)
81 |
82 | return HttpResponse("ERROR get code")
83 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if __name__ == "__main__":
6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "casdemo.settings")
7 | try:
8 | from django.core.management import execute_from_command_line
9 | except ImportError:
10 | # The above import may fail for some other reason. Ensure that the
11 | # issue is really that Django is missing to avoid masking other
12 | # exceptions on Python 2.
13 | try:
14 | import django
15 | except ImportError:
16 | raise ImportError(
17 | "Couldn't import Django. Are you sure it's installed and "
18 | "available on your PYTHONPATH environment variable? Did you "
19 | "forget to activate a virtual environment?"
20 | )
21 | raise
22 | execute_from_command_line(sys.argv)
23 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | django==1.10.8
2 | django-mama-cas
3 | django-uuslug
4 | ldap3
5 | jsonfield
6 |
7 |
--------------------------------------------------------------------------------
/security/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cangelzz/cas-demo-django-server/18774b75fee965048d0ebacdd35370f2286aabe1/security/__init__.py
--------------------------------------------------------------------------------
/security/admin.py:
--------------------------------------------------------------------------------
1 | from .models import *
2 | from .utils import get_user_applications
3 | from base.admin import BaseAdmin
4 | from django.contrib import admin
5 | from django.utils.safestring import mark_safe
6 | from django.db.models import Q
7 | from django.db.models import F
8 | from django.template.loader import render_to_string
9 | from urllib.parse import urljoin
10 |
11 | LIMIT_ADMIN = False
12 |
13 |
14 | class ApplicationAdmin(BaseAdmin):
15 | def users_count(self):
16 | return ACL.objects.filter(Q(roles__application=self) | Q(permissions__application=self)).distinct().count()
17 |
18 | def name(self):
19 | if not self.owner:
20 | return "-"
21 | return self.owner.profile.fullname()
22 |
23 | def managers(self):
24 | return " ".join(list(self.managers.values_list("profile__full_name", flat=True)))
25 |
26 | name.short_description = "开发者"
27 | users_count.short_description = "用户数"
28 | list_display = ["name", "description", "url", "active", users_count, name, managers, "view_permission"]
29 | fields = ["id", "secret", "owner", "name", "description", "url", "view_permission", "active",
30 | "app_visibility", "perm_visibility"]
31 | filter_horizontal = ["managers"]
32 | readonly_fields = ["id", "secret"]
33 | list_filter = [("owner", admin.RelatedOnlyFieldListFilter)]
34 |
35 | def get_fields(self, request, obj=None):
36 | fields = super(ApplicationAdmin, self).get_fields(request, obj)
37 | if request.user.is_superuser and not LIMIT_ADMIN or (obj and obj.owner or None == request.user):
38 | if "managers" not in fields:
39 | fields.insert(5, "managers")
40 | return fields
41 |
42 | def get_readonly_fields(self, request, obj=None):
43 | fields = super(ApplicationAdmin, self).get_readonly_fields(request, obj)
44 | if request.user.is_superuser and not LIMIT_ADMIN:
45 | return fields
46 | else:
47 | return fields + ["owner"]
48 |
49 | def get_queryset(self, request):
50 | qs = super(ApplicationAdmin, self).get_queryset(request)
51 | if request.user.is_superuser and not LIMIT_ADMIN:
52 | return qs
53 |
54 | return get_user_applications(request.user)
55 |
56 | def save_model(self, request, obj, form, change):
57 | if not obj.owner:
58 | obj.owner = request.user
59 | super(ApplicationAdmin, self).save_model(request, obj, form, change)
60 |
61 | def change_view(self, request, object_id, form_url='', extra_context=None):
62 | self.object_id = object_id
63 | return super(ApplicationAdmin, self).change_view(request, object_id, form_url, extra_context)
64 |
65 | def formfield_for_foreignkey(self, db_field, request, **kwargs):
66 | if db_field.name == "view_permission":
67 | if getattr(self, "object_id", None):
68 | kwargs["queryset"] = Permission.objects.filter(application__id=self.object_id)
69 |
70 | return super(ApplicationAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
71 |
72 |
73 | class ApplicationAliasAdmin(BaseAdmin):
74 | list_display = ["application", "name"]
75 | list_filter = [("application", admin.RelatedOnlyFieldListFilter)]
76 |
77 |
78 | class PermissionAdmin(BaseAdmin):
79 | def users(self):
80 | return " ".join(list(self.users.values_list("profile__full_name", flat=True)))
81 |
82 | def user_count(self):
83 | qs = self.assigned_acls.all()
84 | for r in self.roles.all():
85 | qs = qs | r.assigned_acls.all()
86 |
87 | qs = qs.distinct()
88 | cnt = qs.count()
89 | if cnt:
90 | return "%d: %s" % (cnt, " ".join(qs.values_list("user__profile__full_name", flat=True)))
91 |
92 | user_count.short_description = "授权用户"
93 |
94 | list_display = ["application", "name", "description", user_count]
95 | list_filter = ["application__name"]
96 | search_fields = ["name", "description"]
97 |
98 | def get_queryset(self, request):
99 | qs = super(PermissionAdmin, self).get_queryset(request)
100 | if request.user.is_superuser and not LIMIT_ADMIN:
101 | return qs
102 |
103 | return qs.filter(application__in=get_user_applications(request.user))
104 |
105 | def change_view(self, request, object_id, form_url='', extra_context=None):
106 | self.object_id = object_id
107 | return super(PermissionAdmin, self).change_view(request, object_id)
108 |
109 | def formfield_for_foreignkey(self, db_field, request, **kwargs):
110 | if db_field.name == "application":
111 | if request.user.is_superuser and not LIMIT_ADMIN:
112 | pass
113 | else:
114 | kwargs["queryset"] = get_user_applications(request.user)
115 |
116 | return super(PermissionAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
117 |
118 |
119 | class AppUrlAdmin(BaseAdmin):
120 | def full_url(self):
121 | return urljoin(self.application.url, self.url)
122 |
123 | list_display = [full_url, "description"]
124 | full_url.short_description = "完整路径"
125 |
126 | list_filter = [("application", admin.RelatedOnlyFieldListFilter)]
127 | search_fields = ["url", "description"]
128 |
129 | def get_queryset(self, request):
130 | qs = super(AppUrlAdmin, self).get_queryset(request)
131 | if request.user.is_superuser and not LIMIT_ADMIN:
132 | return qs
133 |
134 | return qs.filter(application__in=get_user_applications(request.user))
135 |
136 | def formfield_for_foreignkey(self, db_field, request, **kwargs):
137 | if db_field.name == "application":
138 | if request.user.is_superuser and not LIMIT_ADMIN:
139 | pass
140 | else:
141 | kwargs["queryset"] = get_user_applications(request.user)
142 |
143 | return super(AppUrlAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
144 |
145 |
146 | class RoleAdmin(BaseAdmin):
147 | def users(self):
148 | qs = self.assigned_acls.all()
149 | cnt = qs.count()
150 | if cnt:
151 | return "%d: %s" % (
152 | cnt, " ".join(list(self.assigned_acls.values_list("user__profile__full_name", flat=True))))
153 |
154 | users.short_description = "授权用户"
155 |
156 | list_display = ["application", "name", "description", users]
157 | list_filter = [("application", admin.RelatedOnlyFieldListFilter)]
158 | filter_horizontal = ["permissions", "urls"]
159 | search_fields = ["name", "description"]
160 |
161 | def get_queryset(self, request):
162 | qs = super(RoleAdmin, self).get_queryset(request)
163 | if request.user.is_superuser and not LIMIT_ADMIN:
164 | return qs
165 |
166 | return qs.filter(application__in=get_user_applications(request.user))
167 |
168 | def change_view(self, request, object_id, form_url='', extra_context=None):
169 | self.role = Role.objects.get(id=object_id)
170 | return super(RoleAdmin, self).change_view(request, object_id)
171 |
172 | def formfield_for_foreignkey(self, db_field, request, **kwargs):
173 | if db_field.name == "application":
174 | if request.user.is_superuser and not LIMIT_ADMIN:
175 | pass
176 | else:
177 | kwargs["queryset"] = get_user_applications(request.user)
178 |
179 | return super(RoleAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
180 |
181 | def formfield_for_manytomany(self, db_field, request, **kwargs):
182 | if db_field.name == "permissions":
183 | if getattr(self, "role", None):
184 | kwargs["queryset"] = Permission.objects.filter(application=self.role.application)
185 | else:
186 | kwargs["queryset"] = Permission.objects.filter(application__in=get_user_applications(request.user))
187 |
188 | if db_field.name == "urls":
189 | if getattr(self, "role", None):
190 | kwargs["queryset"] = AppUrl.objects.filter(application=self.role.application)
191 | else:
192 | kwargs["queryset"] = AppUrl.objects.filter(application__in=get_user_applications(request.user))
193 |
194 | return super(RoleAdmin, self).formfield_for_manytomany(db_field, request, **kwargs)
195 |
196 |
197 | class ApplicationListFilter(admin.SimpleListFilter):
198 | title = "应用"
199 |
200 | parameter_name = "app"
201 |
202 | def lookups(self, request, model_admin):
203 | return get_user_applications(request.user).values_list("id", "name")
204 |
205 | def queryset(self, request, queryset):
206 | if self.value():
207 | application = Application.objects.get(id=self.value())
208 | return queryset.filter(Q(roles__application=application) | Q(permissions__application=application))
209 |
210 |
211 | class ACLAdmin(BaseAdmin):
212 | def user_roles(self):
213 | # return mark_safe(
214 | # "
".join(["%s:%s" % (a, r) for a, r in
215 | # self.roles.annotate(display=F("description")).values_list("application__name", "display")]))
216 | items = self.roles.select_related()
217 | return mark_safe(render_to_string("security/includes/admin_acl_list.html", {"items": items}))
218 |
219 | def user_permissions(self):
220 | items = self.permissions.select_related()
221 | return mark_safe(render_to_string("security/includes/admin_acl_list.html", {"items": items}))
222 |
223 | def user_name(self):
224 | return self.user.profile.display_name or self.user.username
225 |
226 | search_fields = ["user__username", "user__profile__full_name"]
227 | filter_horizontal = ["roles", "permissions"]
228 | list_display = [user_name, user_roles, user_permissions]
229 | list_filter = [ApplicationListFilter]
230 | ordering = ["user__profile__xingming"]
231 | exclude = ["urls"]
232 | user_name.short_description = "用户"
233 | user_roles.short_description = "拥有角色"
234 | user_permissions.short_description = "拥有权限"
235 |
236 | def formfield_for_manytomany(self, db_field, request, **kwargs):
237 |
238 | if db_field.name == "roles":
239 | if request.user.is_superuser and not LIMIT_ADMIN:
240 | pass
241 | else:
242 | kwargs["queryset"] = Role.objects.filter(application__in=get_user_applications(request.user))
243 |
244 | if db_field.name == "permissions":
245 | if request.user.is_superuser and not LIMIT_ADMIN:
246 | pass
247 | else:
248 | kwargs["queryset"] = Permission.objects.filter(application__in=get_user_applications(request.user))
249 |
250 | return super(ACLAdmin, self).formfield_for_manytomany(db_field, request, **kwargs)
251 |
252 |
253 | class PropertyAdmin(BaseAdmin):
254 | def acl_username(self):
255 | return self.acl.user.profile.full_name
256 |
257 | acl_username.short_description = "用户"
258 |
259 | def application(self):
260 | return self.application.get_display_name()
261 |
262 | application.short_description = "应用"
263 |
264 | list_display = [acl_username, application, "updated"]
265 | list_filter = [("application", admin.RelatedOnlyFieldListFilter), ("acl", admin.RelatedOnlyFieldListFilter)]
266 | search_fields = ["acl__user__username", "acl__user__profile__full_name", "application__name", "application__description"]
267 | ordering = ["-updated"]
268 |
269 |
270 | class FunctionAdmin(BaseAdmin):
271 | list_display = ["application", "name", "path", "view_permission", "order"]
272 | search_fields = ["name", "path"]
273 | ordering = ["application"]
274 | list_filter = ["application__name"]
275 |
276 | def get_queryset(self, request):
277 | qs = super(FunctionAdmin, self).get_queryset(request)
278 | if request.user.is_superuser and not LIMIT_ADMIN:
279 | return qs
280 |
281 | return qs.filter(application__in=get_user_applications(request.user))
282 |
283 | def change_view(self, request, object_id, form_url='', extra_context=None):
284 | self.function = Function.objects.get(id=object_id)
285 | return super(FunctionAdmin, self).change_view(request, object_id)
286 |
287 | def formfield_for_foreignkey(self, db_field, request, **kwargs):
288 | if db_field.name == "application":
289 | if request.user.is_superuser and not LIMIT_ADMIN:
290 | pass
291 | else:
292 | kwargs["queryset"] = get_user_applications(request.user)
293 |
294 | if db_field.name == "view_permission":
295 | if hasattr(self, "function"):
296 | kwargs["queryset"] = Permission.objects.filter(application=self.function.application)
297 | else:
298 | kwargs["queryset"] = Permission.objects.filter(application__in=get_user_applications(request.user))
299 |
300 | if db_field.name == "parent":
301 | if hasattr(self, "function"):
302 | kwargs["queryset"] = Function.objects.filter(application=self.function.application, folder=True)
303 | else:
304 | kwargs["queryset"] = Function.objects.filter(application__in=get_user_applications(request.user),
305 | folder=True)
306 |
307 | return super(FunctionAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
308 |
309 | admin.site.register(Application, ApplicationAdmin)
310 | admin.site.register(ApplicationAlias, ApplicationAliasAdmin)
311 | admin.site.register(Permission, PermissionAdmin)
312 | admin.site.register(AppUrl, AppUrlAdmin)
313 | admin.site.register(Role, RoleAdmin)
314 | admin.site.register(ACL, ACLAdmin)
315 | admin.site.register(Property, PropertyAdmin)
316 | admin.site.register(Function, FunctionAdmin)
317 |
--------------------------------------------------------------------------------
/security/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class SecurityConfig(AppConfig):
5 | name = 'security'
6 |
--------------------------------------------------------------------------------
/security/cas_callbacks.py:
--------------------------------------------------------------------------------
1 | from .utils import get_roles, get_permissions, get_urls, get_properties
2 | import re
3 | from .models import Application, ApplicationAlias
4 |
5 | pattern = re.compile(r"http[s]?://([\w:-]+)(\.casdemo.com)?")
6 |
7 | def user_security_attributes(user, service):
8 | """Return all available user name related fields and methods."""
9 | application = None
10 | m = pattern.search(service)
11 | if m:
12 | app_name = m.group(1)
13 | try:
14 | application = Application.objects.get(name=app_name)
15 | except Application.DoesNotExist:
16 | try:
17 | alias = ApplicationAlias.objects.get(name=app_name)
18 | application = alias.application
19 | except ApplicationAlias.DoesNotExist:
20 | pass
21 |
22 | attributes = {}
23 | attributes["service"] = service
24 | attributes["app_name"] = application and application.name or ""
25 | attributes["app_id"] = application and application.id.hex or ""
26 | attributes["roles.list"] = get_roles(user, application, type="list")
27 | attributes["permissions.list"] = get_permissions(user, application, type="list")
28 | attributes["urls.list"] = get_urls(user, application, type="list")
29 | attributes["properties"] = get_properties(user, application, type="dict")
30 | return attributes
31 |
--------------------------------------------------------------------------------
/security/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.10.7 on 2017-11-06 02:13
3 | from __future__ import unicode_literals
4 |
5 | from django.conf import settings
6 | from django.db import migrations, models
7 | import django.db.models.deletion
8 | import jsonfield.fields
9 | import uuid
10 |
11 |
12 | class Migration(migrations.Migration):
13 |
14 | initial = True
15 |
16 | dependencies = [
17 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
18 | ('contenttypes', '0002_remove_content_type_name'),
19 | ]
20 |
21 | operations = [
22 | migrations.CreateModel(
23 | name='ACL',
24 | fields=[
25 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
26 | ],
27 | options={
28 | 'verbose_name_plural': '访问控制表',
29 | 'verbose_name': '访问控制项',
30 | },
31 | ),
32 | migrations.CreateModel(
33 | name='Application',
34 | fields=[
35 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
36 | ('name', models.CharField(db_index=True, max_length=50, unique=True, verbose_name='应用名')),
37 | ('description', models.CharField(blank=True, default='', max_length=200, verbose_name='描述')),
38 | ('url', models.URLField(blank=True, default='', verbose_name='网站地址')),
39 | ('active', models.BooleanField(default=True, verbose_name='有效')),
40 | ('secret', models.UUIDField(default=uuid.uuid4, editable=False)),
41 | ('order', models.PositiveSmallIntegerField(default=1, help_text='数值越小,排序越前', verbose_name='排序')),
42 | ('app_visibility', models.BooleanField(default=True, verbose_name='应用默认是否可见')),
43 | ('perm_visibility', models.BooleanField(default=True, verbose_name='应用功能默认是否可用')),
44 | ('managers', models.ManyToManyField(blank=True, related_name='managed_applications', to=settings.AUTH_USER_MODEL, verbose_name='管理员')),
45 | ('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='owned_applications', to=settings.AUTH_USER_MODEL, verbose_name='开发者')),
46 | ],
47 | options={
48 | 'verbose_name_plural': '应用',
49 | 'verbose_name': '应用',
50 | 'ordering': ['order', 'name'],
51 | 'permissions': [('can_access_user_base_info', '访问用户基本信息'), ('can_access_user_detail_info', '访问用户详细信息'), ('can_access_user_security_info', '访问用户安全信息')],
52 | },
53 | ),
54 | migrations.CreateModel(
55 | name='ApplicationAlias',
56 | fields=[
57 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
58 | ('name', models.CharField(db_index=True, max_length=50, unique=True, verbose_name='别名')),
59 | ('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='security.Application', verbose_name='应用')),
60 | ],
61 | options={
62 | 'verbose_name_plural': '应用假名',
63 | 'verbose_name': '应用假名',
64 | },
65 | ),
66 | migrations.CreateModel(
67 | name='AppUrl',
68 | fields=[
69 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
70 | ('description', models.CharField(max_length=50, verbose_name='描述')),
71 | ('url', models.CharField(max_length=500, verbose_name='URL')),
72 | ('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='security.Application', verbose_name='应用')),
73 | ],
74 | options={
75 | 'verbose_name_plural': '路径',
76 | 'verbose_name': '路径',
77 | 'ordering': ['application', 'url'],
78 | },
79 | ),
80 | migrations.CreateModel(
81 | name='Function',
82 | fields=[
83 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
84 | ('name', models.CharField(max_length=50, verbose_name='功能名称')),
85 | ('slug', models.SlugField(verbose_name='英文名称')),
86 | ('path', models.CharField(max_length=1024, verbose_name='功能路径')),
87 | ('order', models.PositiveSmallIntegerField(default=1, help_text='数字越小,排序越前', verbose_name='排序')),
88 | ('folder', models.BooleanField(default=False, verbose_name='是否是目录')),
89 | ('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='functions', to='security.Application', verbose_name='应用')),
90 | ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='security.Function', verbose_name='上级')),
91 | ],
92 | options={
93 | 'verbose_name_plural': '应用功能(菜单)',
94 | 'verbose_name': '应用功能',
95 | 'ordering': ['application', 'order'],
96 | },
97 | ),
98 | migrations.CreateModel(
99 | name='Permission',
100 | fields=[
101 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
102 | ('name', models.CharField(db_index=True, help_text='请使用英文', max_length=200, verbose_name='权限名')),
103 | ('description', models.CharField(blank=True, default='', help_text='中文名称', max_length=200, verbose_name='权限描述')),
104 | ('comment', models.TextField(blank=True, null=True, verbose_name='备注')),
105 | ('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='permissions', to='security.Application', verbose_name='应用')),
106 | ],
107 | options={
108 | 'verbose_name_plural': '权限',
109 | 'verbose_name': '权限',
110 | 'ordering': ['application', 'name'],
111 | },
112 | ),
113 | migrations.CreateModel(
114 | name='Property',
115 | fields=[
116 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
117 | ('content', jsonfield.fields.JSONField(blank=True, default={}, null=True, verbose_name='自定义内容')),
118 | ('created', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
119 | ('updated', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
120 | ('acl', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='properties', to='security.ACL', verbose_name='属性')),
121 | ('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='security.Application', verbose_name='应用')),
122 | ],
123 | options={
124 | 'verbose_name_plural': '属性',
125 | 'verbose_name': '属性',
126 | },
127 | ),
128 | migrations.CreateModel(
129 | name='Role',
130 | fields=[
131 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
132 | ('name', models.CharField(db_index=True, help_text='请使用英文', max_length=200, verbose_name='角色名')),
133 | ('description', models.CharField(blank=True, default='', help_text='请使用中文,不建议太长', max_length=200, verbose_name='角色中文描述')),
134 | ('comment', models.TextField(blank=True, null=True, verbose_name='备注')),
135 | ('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='roles', to='security.Application', verbose_name='应用')),
136 | ('permissions', models.ManyToManyField(blank=True, related_name='roles', to='security.Permission', verbose_name='角色拥有权限')),
137 | ('urls', models.ManyToManyField(blank=True, related_name='roles', to='security.AppUrl', verbose_name='角色可访问的URL')),
138 | ],
139 | options={
140 | 'verbose_name_plural': '角色',
141 | 'verbose_name': '角色',
142 | 'ordering': ['application', 'name'],
143 | },
144 | ),
145 | migrations.AddField(
146 | model_name='function',
147 | name='view_permission',
148 | field=models.ForeignKey(blank=True, help_text='空为对所有用户实行默认可见状态,否则仅对拥有该权限的用户可见', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='security.Permission', verbose_name='对应权限'),
149 | ),
150 | migrations.AddField(
151 | model_name='application',
152 | name='view_permission',
153 | field=models.ForeignKey(blank=True, help_text='空为对所有用户实行默认可见状态,否则仅对拥有该权限的用户可见', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='security.Permission', verbose_name='可见权限'),
154 | ),
155 | migrations.AddField(
156 | model_name='acl',
157 | name='permissions',
158 | field=models.ManyToManyField(blank=True, related_name='assigned_acls', to='security.Permission', verbose_name='权限'),
159 | ),
160 | migrations.AddField(
161 | model_name='acl',
162 | name='roles',
163 | field=models.ManyToManyField(blank=True, related_name='assigned_acls', to='security.Role', verbose_name='角色'),
164 | ),
165 | migrations.AddField(
166 | model_name='acl',
167 | name='urls',
168 | field=models.ManyToManyField(blank=True, related_name='assigned_urls', to='security.AppUrl', verbose_name='路径'),
169 | ),
170 | migrations.AddField(
171 | model_name='acl',
172 | name='user',
173 | field=models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='域账户'),
174 | ),
175 | migrations.AlterUniqueTogether(
176 | name='role',
177 | unique_together=set([('application', 'name')]),
178 | ),
179 | migrations.AlterIndexTogether(
180 | name='role',
181 | index_together=set([('application', 'name')]),
182 | ),
183 | migrations.AlterUniqueTogether(
184 | name='property',
185 | unique_together=set([('acl', 'application')]),
186 | ),
187 | migrations.AlterUniqueTogether(
188 | name='permission',
189 | unique_together=set([('application', 'name')]),
190 | ),
191 | migrations.AlterIndexTogether(
192 | name='permission',
193 | index_together=set([('application', 'name')]),
194 | ),
195 | migrations.AlterUniqueTogether(
196 | name='function',
197 | unique_together=set([('application', 'slug'), ('application', 'name')]),
198 | ),
199 | migrations.AlterUniqueTogether(
200 | name='appurl',
201 | unique_together=set([('application', 'url')]),
202 | ),
203 | ]
204 |
--------------------------------------------------------------------------------
/security/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cangelzz/cas-demo-django-server/18774b75fee965048d0ebacdd35370f2286aabe1/security/migrations/__init__.py
--------------------------------------------------------------------------------
/security/models.py:
--------------------------------------------------------------------------------
1 | import uuid
2 | from django.db import models
3 | from django.contrib.auth.models import User
4 | from django.core.validators import validate_slug
5 | from django.db.models.signals import post_save
6 | from urllib.parse import urljoin
7 | from django.db.models import Q
8 | from jsonfield import JSONField
9 |
10 |
11 | class Application(models.Model):
12 | class Meta:
13 | verbose_name = "应用"
14 | verbose_name_plural = "应用"
15 | ordering = ["order", "name"]
16 | permissions = [
17 | ("can_access_user_base_info", "访问用户基本信息"),
18 | ("can_access_user_detail_info", "访问用户详细信息"),
19 | ("can_access_user_security_info", "访问用户安全信息")
20 | ]
21 |
22 | id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
23 | name = models.CharField(max_length=50, unique=True, db_index=True, verbose_name="应用名")
24 | description = models.CharField(max_length=200, blank=True, default="", verbose_name="描述")
25 | url = models.URLField(blank=True, default="", verbose_name="网站地址")
26 | active = models.BooleanField(default=True, verbose_name="有效")
27 | owner = models.ForeignKey(User, on_delete=models.SET_NULL, verbose_name="开发者", null=True, blank=True,
28 | related_name="owned_applications")
29 | managers = models.ManyToManyField(User, verbose_name="管理员", blank=True, related_name="managed_applications")
30 | secret = models.UUIDField(default=uuid.uuid4, editable=False)
31 | order = models.PositiveSmallIntegerField(default=1, verbose_name="排序", help_text="数值越小,排序越前")
32 | view_permission = models.ForeignKey("Permission", verbose_name="可见权限", null=True, blank=True,
33 | help_text="空为对所有用户实行默认可见状态,否则仅对拥有该权限的用户可见", related_name="+")
34 | app_visibility = models.BooleanField(default=True, verbose_name="应用默认是否可见")
35 | perm_visibility = models.BooleanField(default=True, verbose_name="应用功能默认是否可用")
36 |
37 | def __str__(self):
38 | return self.name
39 |
40 | def short_id(self):
41 | return self.id.hex[:6]
42 |
43 | def has_permission(self, user):
44 | from .utils import get_permissions
45 | if not self.view_permission:
46 | return self.app_visibility
47 | return get_permissions(user, self).filter(id=self.view_permission.id).exists()
48 |
49 | def root_functions(self, user=None):
50 | functions = Function.objects.filter(application=self, parent__isnull=True).order_by("order")
51 | if not user:
52 | return functions
53 |
54 | from .utils import get_permissions
55 | return functions.filter(Q(view_permission__isnull=True) | Q(view_permission__in=get_permissions(user, self)))
56 |
57 | def can_manage(self, user):
58 | return self.owner == user or self.managers.filter(id=user.id).exists() or user.is_superuser
59 |
60 | def get_display_name(self):
61 | return self.description or self.name
62 |
63 | def valid_acls(self):
64 | return (ACL.objects.filter(roles__application=self) | ACL.objects.filter(permissions__application=self)).filter(
65 | user__is_active=True).distinct().order_by("user__profile__xingming")
66 |
67 |
68 | class ApplicationAlias(models.Model):
69 | application = models.ForeignKey(Application, verbose_name="应用")
70 | name = models.CharField(max_length=50, verbose_name="别名", unique=True, db_index=True)
71 |
72 | class Meta:
73 | verbose_name = "应用假名"
74 | verbose_name_plural = "应用假名"
75 |
76 | def __str__(self):
77 | return self.name + " : " + self.application.name
78 |
79 |
80 | class Permission(models.Model):
81 | class Meta:
82 | verbose_name = "权限"
83 | verbose_name_plural = "权限"
84 | ordering = ["application", "name"]
85 | unique_together = ["application", "name"]
86 | index_together = ["application", "name"]
87 |
88 | application = models.ForeignKey(Application, related_name="permissions", verbose_name="应用")
89 | name = models.CharField(max_length=200, db_index=True, verbose_name="权限名", help_text="请使用英文")
90 | description = models.CharField(max_length=200, blank=True, default="", verbose_name="权限描述", help_text="中文名称")
91 | comment = models.TextField(verbose_name="备注", null=True, blank=True)
92 |
93 | def __str__(self):
94 | return "%s:%s" % (self.application, self.name)
95 |
96 | def get_display_name(self):
97 | return self.description or self.name
98 |
99 | @property
100 | def sorted_acls(self):
101 | return self.assigned_acls.order_by("user__profile__xingming")
102 |
103 | def granted_acls(self):
104 | acls = self.assigned_acls.all()
105 | for role in self.roles.all():
106 | acls |= role.assigned_acls.all()
107 |
108 | acls = acls.distinct()
109 | return acls
110 |
111 | def inherited_acls(self):
112 | acls = ACL.objects.none()
113 | for role in self.roles.all():
114 | acls |= role.assigned_acls.all()
115 |
116 | acls = acls.distinct().order_by("user__profile__xingming")
117 | return acls
118 |
119 |
120 | class AppUrl(models.Model):
121 | class Meta:
122 | verbose_name = "路径"
123 | verbose_name_plural = "路径"
124 | ordering = ["application", "url"]
125 | unique_together = ["application", "url"]
126 |
127 | application = models.ForeignKey(Application, verbose_name="应用")
128 | description = models.CharField(max_length=50, verbose_name="描述")
129 | url = models.CharField(max_length=500, verbose_name="URL")
130 |
131 | def __str__(self):
132 | return urljoin(self.application.url, self.url)
133 |
134 |
135 | class Role(models.Model):
136 | class Meta:
137 | verbose_name = "角色"
138 | verbose_name_plural = "角色"
139 | ordering = ["application", "name"]
140 | unique_together = ["application", "name"]
141 | index_together = ["application", "name"]
142 |
143 | application = models.ForeignKey(Application, related_name="roles", verbose_name="应用")
144 | name = models.CharField(max_length=200, db_index=True, verbose_name="角色名", help_text="请使用英文")
145 | description = models.CharField(max_length=200, blank=True, default="", verbose_name="角色中文描述", help_text="请使用中文,不建议太长")
146 | comment = models.TextField(verbose_name="备注", null=True, blank=True)
147 | permissions = models.ManyToManyField(Permission, related_name="roles", blank=True, verbose_name="角色拥有权限")
148 | urls = models.ManyToManyField(AppUrl, related_name="roles", blank=True, verbose_name="角色可访问的URL")
149 |
150 | def __str__(self):
151 | return "%s:%s" % (self.application, self.name)
152 |
153 | def get_display_name(self):
154 | return self.description or self.name
155 |
156 | @property
157 | def sorted_acls(self):
158 | return self.assigned_acls.order_by("user__profile__xingming")
159 |
160 |
161 | class ACL(models.Model):
162 | class Meta:
163 | verbose_name = "访问控制项"
164 | verbose_name_plural = "访问控制表"
165 |
166 | user = models.OneToOneField(User, verbose_name="域账户", editable=False)
167 | roles = models.ManyToManyField(Role, blank=True, related_name="assigned_acls", verbose_name="角色")
168 | permissions = models.ManyToManyField(Permission, blank=True, related_name="assigned_acls", verbose_name="权限")
169 | urls = models.ManyToManyField(AppUrl, blank=True, related_name="assigned_urls", verbose_name="路径")
170 |
171 | def __str__(self):
172 | return self.user.profile.fullname()
173 |
174 |
175 | class Property(models.Model):
176 | acl = models.ForeignKey(ACL, related_name="properties", verbose_name="属性")
177 | application = models.ForeignKey(Application, verbose_name="应用")
178 | content = JSONField(verbose_name="自定义内容", null=True, blank=True, default={})
179 | created = models.DateTimeField(verbose_name="创建时间", auto_now_add=True)
180 | updated = models.DateTimeField(verbose_name="更新时间", auto_now=True)
181 |
182 | class Meta:
183 | verbose_name = "属性"
184 | verbose_name_plural = "属性"
185 | unique_together = [("acl", "application")]
186 |
187 |
188 | class Function(models.Model):
189 | class Meta:
190 | verbose_name = "应用功能"
191 | verbose_name_plural = "应用功能(菜单)"
192 | unique_together = [("application", "name"), ("application", "slug")]
193 | ordering = ["application", "order"]
194 |
195 | application = models.ForeignKey(Application, related_name="functions", verbose_name="应用")
196 | name = models.CharField(max_length=50, verbose_name="功能名称")
197 | slug = models.SlugField(max_length=50, verbose_name="英文名称")
198 | path = models.CharField(max_length=1024, verbose_name="功能路径")
199 | view_permission = models.ForeignKey(Permission, verbose_name="对应权限", null=True, blank=True,
200 | help_text="空为对所有用户实行默认可见状态,否则仅对拥有该权限的用户可见", related_name="+")
201 | order = models.PositiveSmallIntegerField(default=1, verbose_name="排序", help_text="数字越小,排序越前")
202 | folder = models.BooleanField(default=False, verbose_name="是否是目录")
203 | parent = models.ForeignKey('self', null=True, blank=True, verbose_name="上级")
204 |
205 | def __str__(self):
206 | return "%s: %s" % (self.application.description, self.name)
207 |
208 | def full_path(self):
209 | if self.path.startswith("http") or self.path.startswith("//"):
210 | return self.path
211 | return urljoin(self.application.url, self.path)
212 |
213 | def has_permission(self, user):
214 | from .utils import get_permissions
215 | if not self.view_permission:
216 | return self.application.perm_visibility
217 | return get_permissions(user, self).filter(id=self.view_permission.id).exists()
218 |
219 | def sub_functions(self, user=None):
220 | functions = self.function_set.order_by("order")
221 | if not user:
222 | return functions
223 |
224 | from .utils import get_permissions
225 | return functions.filter(Q(view_permission__isnull=True) | Q(view_permission__in=get_permissions(user, self.application)))
--------------------------------------------------------------------------------
/security/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/security/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls import url
2 | from django.contrib.auth.decorators import login_required
3 | from . import views
4 | from django.conf import settings
5 |
6 | urlpatterns = [
7 | url(r'^acl/add/$', login_required(views.add_acl), name="add_acl"),
8 | url(r'^acl/remove/$', login_required(views.remove_acl), name="remove_acl")
9 | ]
10 |
11 | urlpatterns += [
12 | url(r'^api/validate_user_permissions/$', views.api_validate_user_permissions),
13 | url(r'^api/validate_user_roles/$', views.api_validate_user_roles),
14 | url(r'^api/auth/$', views.api_auth_user),
15 | url(r'^api/query_user/(?P[\w.-]+)/$', views.api_query_user),
16 | url(r'^api/urls/$', views.api_urls),
17 | url(r'^api/permissions/add/$', views.api_permissions_add),
18 | url(r'^api/user/$', views.api_user),
19 | url(r'^api/users/', views.api_users)
20 | ]
21 |
--------------------------------------------------------------------------------
/security/utils.py:
--------------------------------------------------------------------------------
1 | from .models import *
2 | import json
3 |
4 |
5 | def _json(queryset, key):
6 | result = {}
7 | for q in queryset:
8 | result.setdefault(q.application.name, [])
9 | result[q.application.name].append(q.name)
10 |
11 | result_list = []
12 | for k, v in result.items():
13 | result_list.append({"application": k, key: result[k]})
14 | return result_list
15 |
16 |
17 | def _return(queryset, key, type):
18 | if type == "queryset":
19 | return queryset
20 |
21 | if type == "text":
22 | if key == "urls":
23 | from urllib.parse import urljoin
24 | result = "|".join([urljoin(a, p) for a, p in queryset.values_list("application__url", "url")])
25 | else:
26 | result = "|".join(["%s:%s" % t for t in queryset.values_list("application__name", "name")])
27 | return result and result or ""
28 |
29 | if type == "dict":
30 | return _json(queryset, key)
31 |
32 | if type == "list":
33 | if key == "urls":
34 | return list(queryset.values_list("url", flat=True))
35 | else:
36 | return list(queryset.values_list("name", flat=True))
37 |
38 |
39 | def get_roles(user, application_name=None, type="queryset"):
40 | if application_name:
41 | if isinstance(application_name, Application):
42 | queryset = user.acl.roles.filter(application=application_name)
43 | else:
44 | queryset = user.acl.roles.filter(application__name=application_name)
45 | else:
46 | queryset = user.acl.roles.all()
47 | return _return(queryset.distinct(), "roles", type)
48 |
49 |
50 | def get_permissions(user, application_name=None, type="queryset"):
51 | if application_name:
52 | if isinstance(application_name, Application):
53 | params = {"application": application_name}
54 | else:
55 | params = {"application__name": application_name}
56 |
57 | queryset = user.acl.permissions.filter(**params)
58 | for role in user.acl.roles.filter(**params):
59 | queryset = queryset | role.permissions.filter(**params)
60 | else:
61 | queryset = user.acl.permissions.all()
62 | for role in user.acl.roles.all():
63 | queryset = queryset | role.permissions.all()
64 |
65 | return _return(queryset.distinct(), "permissions", type)
66 |
67 |
68 | def get_urls(user, application_name=None, type="queryset"):
69 | if application_name:
70 | roles = user.acl.roles.filter(application__name=application_name)
71 | else:
72 | return []
73 |
74 | queryset = AppUrl.objects.none()
75 | for role in roles:
76 | queryset = queryset | role.urls.all()
77 |
78 | return _return(queryset.distinct(), "urls", type)
79 |
80 |
81 | def get_properties(user, application_name=None, type="queryset"):
82 | def _loads(data):
83 | try:
84 | return json.loads(data)
85 | except:
86 | return {}
87 |
88 | if application_name:
89 | if isinstance(application_name, Application):
90 | params = {"application": application_name}
91 | else:
92 | params = {"application__name": application_name}
93 |
94 | queryset = Property.objects.filter(acl=user.acl, **params)
95 | else:
96 | queryset = Property.objects.filter(acl=user.acl)
97 |
98 | if type == "queryset":
99 | return queryset
100 | elif type == "list":
101 | return list(map(lambda x: {"application": x[0], "properties": _loads(x[1])}, queryset.values_list("application__name", "content")))
102 | elif type == "dict":
103 | return queryset.first() and queryset.first().content or {}
104 |
105 |
106 | def get_user_applications(user):
107 | return (user.owned_applications.all() | user.managed_applications.all()).distinct()
--------------------------------------------------------------------------------
/security/views.py:
--------------------------------------------------------------------------------
1 | from django.http.response import *
2 | from django.template.response import TemplateResponse
3 | from django.views.decorators.csrf import csrf_exempt
4 | from django.shortcuts import get_object_or_404
5 | from django.contrib.auth import authenticate
6 | from django.views.generic import ListView
7 | from dingding.client import ClientWrapper
8 | import re
9 | from .utils import *
10 | from django.db.models import F
11 | from django.conf import settings
12 | from django.db.models import Q
13 | from .models import *
14 |
15 |
16 | @csrf_exempt
17 | def api_auth_user(request):
18 | username = request.POST.get("username")
19 | password = request.POST.get("password")
20 | if not username or not password:
21 | return JsonResponse({"ret": False, "errmsg": "Empty username or password"})
22 |
23 | user = authenticate(username=username, password=password)
24 | if user is None:
25 | return JsonResponse({"ret": False, "errmsg": "Bad username or password"})
26 |
27 | return JsonResponse({"ret": True, "errmsg": "user authenticated"})
28 |
29 |
30 | def auth_application(func):
31 | @csrf_exempt
32 | def _auth(request, *args, **kwargs):
33 | app_id = request.META.get("HTTP_APPID", request.GET.get("appId"))
34 | if not app_id:
35 | return HttpResponseBadRequest("No APPID in the header or querystring")
36 |
37 | app_secret = request.META.get("HTTP_APPSECRET", request.GET.get("appSecret"))
38 | if not app_secret:
39 | return HttpResponseBadRequest("No APPSECRET in the header or querystring")
40 | try:
41 | app = Application.objects.get(id=app_id, secret=app_secret)
42 | if not app.active:
43 | return HttpResponseForbidden("Application is disabled")
44 | except Application.DoesNotExist:
45 | return HttpResponseBadRequest("Wrong APPID or APPSECRET")
46 | except:
47 | return HttpResponseBadRequest("Bad APPID or APPSECRET")
48 |
49 | return func(request, app, *args, **kwargs)
50 |
51 | return _auth
52 |
53 |
54 | @auth_application
55 | def api_query_user(request, app, username):
56 | user = get_object_or_404(User, username=username)
57 | return JsonResponse(
58 | {
59 | "roles.list": get_roles(user, app.name, "list"),
60 | "permissions.list": get_permissions(user, app.name, "list"),
61 | "urls.list": get_urls(user, app.name, "list"),
62 | "properties": get_properties(user, app.name, "dict")
63 | }
64 | )
65 |
66 |
67 | @auth_application
68 | def api_validate_user_permissions(request, app=None):
69 | username = request.GET.get("username", request.GET.get("userId", ""))
70 | try:
71 | user = User.objects.get(username=username)
72 | except User.DoesNotExist:
73 | return JsonResponse({"ret": False, "errmsg": "No such user: " + username})
74 |
75 | permissions = request.GET.get("permissions")
76 | if not permissions:
77 | return JsonResponse({"ret": False, "errmsg": "No permissions in the querystring"})
78 |
79 | type = request.GET.get("type", "all")
80 | if not type in ["all", "any"]:
81 | return JsonResponse({"ret": False, "errmsg": "Type %s is not available" % type})
82 |
83 | permissions = set(filter(None, permissions.split("|")))
84 | user_permissions = set(get_permissions(user, app, "list"))
85 |
86 | if type == "all":
87 | invalid = permissions.difference(user_permissions)
88 | if len(invalid) > 0:
89 | return JsonResponse({"ret": False, "errmsg": "Not have: %s permission[s]" % "|".join(invalid)})
90 | else:
91 | return JsonResponse({"ret": True, "errmsg": "User has all the permissions"})
92 |
93 | if type == "any":
94 | intersect = permissions.intersection(user_permissions)
95 | if len(intersect) > 0:
96 | return JsonResponse({"ret": True, "errmsg": "User has %s permission[s]" % "|".join(intersect)})
97 | else:
98 | return JsonResponse({"ret": False, "errmsg": "User does not have any permissions"})
99 |
100 | return HttpResponseBadRequest("You should not see this")
101 |
102 |
103 | @auth_application
104 | def api_validate_user_roles(request, app=None):
105 | username = request.GET.get("username", request.GET.get("userId", ""))
106 | try:
107 | user = User.objects.get(username=username)
108 | except User.DoesNotExist:
109 | return JsonResponse({"ret": False, "errmsg": "No such user: " + username})
110 |
111 | roles = request.GET.get("roles")
112 | if not roles:
113 | return JsonResponse({"ret": False, "errmsg": "No roles in the querystring"})
114 |
115 | type = request.GET.get("type", "all")
116 | if not type in ["all", "any"]:
117 | return JsonResponse({"ret": False, "errmsg": "Type %s is not available" % type})
118 |
119 | roles = set(filter(None, roles.split("|")))
120 | user_roles = get_roles(user, app, "list")
121 |
122 | if type == "all":
123 | invalid = set(roles).difference(user_roles)
124 | if len(invalid) > 0:
125 | return JsonResponse({"ret": False, "errmsg": "Not have: %s role[s]" % ("|").join(invalid)})
126 | else:
127 | return JsonResponse({"ret": True, "errmsg": "User has all the roles"})
128 |
129 | if type == "any":
130 | intersect = set(roles).intersection(user_roles)
131 | if len(intersect) > 0:
132 | return JsonResponse({"ret": True, "errmsg": "User has %s role[s]" % "|".join(intersect)})
133 | else:
134 | return JsonResponse({"ret": False, "errmsg": "User does not have any roles"})
135 |
136 | return HttpResponseBadRequest("You should not see this")
137 |
138 |
139 | @auth_application
140 | def api_urls(request, app=None):
141 | username = request.GET.get("username")
142 | if not username:
143 | return JsonResponse({"status": "error", "msg": "no username"})
144 |
145 | try:
146 | user = User.objects.get(username=username)
147 | except User.DoesNotExist:
148 | return JsonResponse({"status": "error", "msg": "user %s does not exist" % username})
149 |
150 | user_urls = get_urls(user, app.name, "list")
151 |
152 | return JsonResponse({"status": "ok", "urls.list": user_urls})
153 |
154 |
155 | @auth_application
156 | def api_permissions_add(request, app=None):
157 | name = request.POST.get("name")
158 | if not name:
159 | return HttpResponseBadRequest("Permission name is missing")
160 |
161 | description = request.POST.get("description")
162 | permission, new = Permission.objects.get_or_create(application=app, name=name,
163 | defaults={"description": description})
164 | if not new:
165 | permission.description = description
166 | permission.save(update_fields=["description"])
167 |
168 | return JsonResponse(
169 | {"status": new and "created" or "updated", "name": name, "description": description, "app": app.name})
170 |
171 |
172 | @csrf_exempt
173 | def add_acl(request):
174 | if not request.method == "POST":
175 | return HttpResponseBadRequest("不支持的方法")
176 |
177 | type = request.POST.get("type")
178 | if not type in ["role", "permission"]:
179 | return HttpResponseBadRequest("类型错误")
180 |
181 | acl = get_object_or_404(ACL, user__username=request.POST.get("username"))
182 | object_id = request.POST.get("id")
183 | if type == "role":
184 | role = get_object_or_404(Role, id=object_id)
185 | if request.user.is_superuser or role.application.can_manage(request.user):
186 | acl.roles.add(role)
187 | return TemplateResponse(request, "security/includes/role.html", {"role": role})
188 |
189 | if type == "permission":
190 | permission = get_object_or_404(Permission, id=object_id)
191 | if request.user.is_superuser or permission.application.can_manage(request.user):
192 | acl.permissions.add(permission)
193 | return TemplateResponse(request, "security/includes/permission.html", {"permission": permission})
194 |
195 | return HttpResponseForbidden("没有权限进行此操作")
196 |
197 |
198 | @csrf_exempt
199 | def remove_acl(request):
200 | if not request.method == "POST":
201 | return HttpResponseBadRequest("不支持的方法")
202 |
203 | type = request.POST.get("type")
204 | if not type in ["role", "permission"]:
205 | return HttpResponseBadRequest("类型错误")
206 |
207 | acl = get_object_or_404(ACL, user__username=request.POST.get("username"))
208 | object_id = request.POST.get("id")
209 | if type == "role":
210 | role = get_object_or_404(Role, id=object_id)
211 | if request.user.is_superuser or role.application.can_manage(request.user):
212 | acl.roles.remove(role)
213 | return HttpResponse("OK")
214 |
215 | if type == "permission":
216 | permission = get_object_or_404(Permission, id=object_id)
217 | if request.user.is_superuser or permission.application.can_manage(request.user):
218 | acl.permissions.remove(permission)
219 | return HttpResponse("OK")
220 |
221 | return HttpResponseBadRequest("操作未成功")
222 |
223 |
224 | @auth_application
225 | def api_user(request, app):
226 | username = request.GET.get("username")
227 | if not username:
228 | return JsonResponse({"status": "error", "msg": "no username"})
229 |
230 | try:
231 | user = User.objects.get(username=username)
232 | except User.DoesNotExist:
233 | return JsonResponse({"status": "error", "msg": "user %s does not exist" % username})
234 |
235 | result = {
236 | "username": username,
237 | "fullname": user.profile.fullname(),
238 | "name": user.profile.full_name,
239 | "mobile": user.dinguser.mobile or user.profile.mobile,
240 | "ding_user_id": user.dinguser.user_id,
241 | "avatar": user.dinguser.avatar_url,
242 | "roles": get_roles(user, app.name, "list"),
243 | "permissions": get_permissions(user, app.name, "list"),
244 | "urls": get_urls(user, app.name, "list"),
245 | "properties": get_properties(user, app.name, "dict"),
246 | "department": user.profile.ou.name
247 | }
248 | return JsonResponse(result)
249 |
250 |
251 | @auth_application
252 | def api_users(request, app):
253 | result = {}
254 | role_name = request.GET.get("role")
255 | if role_name:
256 | try:
257 | result["role"] = role_name
258 | role = Role.objects.get(application=app, name=role_name)
259 | result["users"] = list(role.assigned_acls.values_list("user__username", flat=True))
260 |
261 | except Role.DoesNotExist:
262 | result["users"] = []
263 |
264 | permission_name = request.GET.get("permission")
265 | if permission_name:
266 | try:
267 | result["permission"] = permission_name
268 | permission = Permission.objects.get(application=app, name=permission_name)
269 | acls = permission.assigned_acls.all()
270 | for role in permission.roles.all():
271 | acls = acls | role.assigned_acls.all()
272 |
273 | acls = acls.distinct()
274 | result["users"] = list(
275 | acls.annotate(username=F("user__username"), name=F("user__profile__full_name"), department=F("user__profile__ou__name")).values("username",
276 | "name",
277 | "department"))
278 |
279 | except Permission.DoesNotExist:
280 | result["users"] = []
281 |
282 | return JsonResponse(result)
283 |
--------------------------------------------------------------------------------