├── .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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | 43 |
44 |
45 |
46 |
47 |
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 | 5 | 6 | 7 | {% endfor %} 8 |
{{ item.application.get_display_name }}{{ item.get_display_name }}
-------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------