关注公众号,将获取更多运维干货
实现流程
编写OAuth2授权接口
ldap.py
from fastapi import APIRouter,Depends
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from .extensions import ldap_api
from .extensions.ldap_api import Token
from typing import Dict
from openldap.config import cnIn,DnInfo
'''实例化OAuth2PasswordBearer,接收URL,并返回用户token信息'''
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/ldap/token")
@router.post("/token",description="验证用户密码是否可以连接,验证成功 返回json信息")
def login(form_data: OAuth2PasswordRequestForm = Depends()):
username = form_data.username
password = form_data.password
login = ldap_api.Login(username,password)
result = login.login()
return result
config.py
定义ldap的配置信息和请求模型
from enum import Enum
from pydantic import BaseModel,EmailStr,Field
from typing import Optional
from .extensions import ldap_api
ldap_config = {
"host" : "192.168.253.235",
"post" : 636,
"base_dn" : 'dc=demo,dc=cn',
"user" : "cn=admin,dc=demo,dc=cn",
"password" : "12345678",
"use_ssl" : True,
}
baseDn = {
"basedn": "dc=demo,dc=cn"
}
basednGroup = {
"basedn": "ou=Group,dc=demo,dc=cn"
}
basednPeople = {
"basedn": "ou=People,dc=demo,dc=cn"
}
class BaseOu(str,Enum):
'''继承string和enum的子类'''
Group = 'Group'
People = 'People'
Manager = 'Manager'
class DnInfo(BaseModel):
'''定义dn请求体'''
groupName: str
cnName: str
ouName: str
class Config:
schema_extra = {
"example": {
"groupName": "测试组",
"cnName": "lujun",
"ouName": "测试组"
}
}
class cnOut(BaseModel):
'''用户信息模型'''
sn: str
mail: EmailStr = "string@demo.cn"
cn: str
title: str
uid: str
displayName: str
description: Optional[str] = None
departmentOu: str = Field(description="ou字段,组织架构,比如运维组")
class cnIn(cnOut):
'''用户信息模型,添加密码字段'''
userPassword: str
ldap_api.py
增删改查实现的逻辑都在该文件下
from ldap3 import Server, Connection,ALL_ATTRIBUTES,SUBTREE,Attribute,MODIFY_REPLACE,HASHED_SALTED_SHA
from ldap3.utils.hashed import hashed
from openldap.config import ldap_config,basednPeople,basednGroup,baseDn
from ldap3.core.exceptions import LDAPBindError
import json
from fastapi import HTTPException,status,Depends
from pydantic import EmailStr
import base64
def BadRequesHttpException():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="该用户还未注册!!!",
headers={"WWW-Authenticate": "Bearer"}
)
class LdapGetUsers(object):
def __init__(self):
'''初始化LDAP连接,这边用admin用户初始化Connection,需要利用该方法查询用户属于哪个ou'''
self.conn = Connection(
Server(
host=ldap_config["host"],
port=ldap_config["post"],
use_ssl=ldap_config["use_ssl"],
),
user=ldap_config["user"],
password=ldap_config["password"],
)
self.conn.bind()
def get_users(self,username):
'''
作用:
查询某个用户信息,这边用来获取登录用户属于哪个ou,需要有admin权限,所以单独初始化LDAP Connection
参数说明:
self.conn.entries: 列出所有ou和cn信息,需要for遍历
entry_to_json: 打印json形式
:return 返回用户信息,失败返回False
'''
try:
entryUserList = {}
self.conn.search(
search_base=f'{baseDn["basedn"]}',
search_filter="(objectClass=inetOrgPerson)",
attributes = ALL_ATTRIBUTES,
search_scope=SUBTREE,
)
for entry in self.conn.entries:
entryUserList.update({json.loads(entry.entry_to_json())['attributes']['cn'][0]:json.loads(entry.entry_to_json())['attributes']})
if username in entryUserList.keys():
self.conn.unbind
return entryUserList[username]
else:
self.conn.unbind
return False
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=e
)
class LdapConfig(object):
'''
作用:
根据用户输入username和password,初始化ldap连接
'''
def __init__(self,username,password):
self.username = username
self.password = password
if self.username == "admin":
#加载admin用户Connection
self.conn = LdapGetUsers().conn
self.conn.bind()
else:
ldap = LdapGetUsers()
usersInfo = ldap.get_users(self.username)
if usersInfo == False:
BadRequesHttpException()
else:
self.conn = Connection(
Server(
host=ldap_config["host"],
port=ldap_config["post"],
use_ssl=ldap_config["use_ssl"],
),
user=f'cn={self.username},ou={usersInfo["ou"][0]},{basednPeople["basedn"]}',
password=self.password,
)
self.conn.bind()
class Token:
@staticmethod
def Encryption(token: str):
'''加密 返回一串token'''
token = base64.b64encode(token.encode("utf-8"))
return token.decode("utf-8")
@staticmethod
def Decryption(token: str):
'''解密 返回dict'''
token = base64.b64decode(token).decode("utf-8")
return json.loads(token)
class Login(LdapConfig):
def login(self):
'''
作用:
验证用户是否可以连接成功,并返回加密token信息,后续发送请求,需要携带该token信息,通过身份验证
:return json类型,携带token_type令牌类型,必须携带
:return json类型,携带access_token包含token字符串,必须携带
解释:
401验证错误返回,携带headers={"WWW-Authenticate": "Bearer"},不是必须的,只是符合OAuth2规范
{usersInfo["ou"][0]}: 返回该登录用户所属的ou,登录使用
'''
try:
if self.conn.bind(): #为True是查询成功
#userInfo 返回一个加密后的token
userInfo = Token.Encryption(json.dumps({"username": self.username, "password": self.password}))
return {"access_token": userInfo, "token_type": "bearer","result": self.conn.bind()}
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="密码不正确!!!",
headers = {"WWW-Authenticate": "Bearer"}
)
except LDAPBindError:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Openldap连接异常!!!",
headers={"WWW-Authenticate": "Bearer"}
)
class LDAP(LdapConfig):
'''
如果要重写LdapConfig__init__,注意要继承父类的构造方法.
格式:
super(子类,self).__init__(参数1,参数2,....)
'''
def modify_password(self,username,new_password):
'''修改用户密码'''
ldap = LdapGetUsers()
usersInfo = ldap.get_users(username)
if usersInfo == False:
BadRequesHttpException()
else:
hashed_password = hashed(HASHED_SALTED_SHA, new_password) #hash加密
self.conn.modify(f'cn={username},ou={usersInfo["ou"][0]},{basednPeople["basedn"]}', {'userPassword': [(MODIFY_REPLACE, [hashed_password])]})
return self.conn.result
def delete_groups(self,groupName):
'''
作用:
删除Groups
参数介绍:
groupsName = cn
'''
self.conn.delete(dn=f'cn={groupName},{basednGroup["basedn"]}')
return self.conn.result
def delete_users(self,username: str):
'''删除LDAP用户'''
try:
#查询要删除的用户是否哪个ou(组织)下
ldap = LdapGetUsers()
usersInfo = ldap.get_users(username)
if usersInfo == False:
BadRequesHttpException()
self.conn.delete(dn=f'cn={username},ou={usersInfo["ou"][0]},{basednPeople["basedn"]}')
return self.conn.result
except LDAPBindError:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Openldap连接异常!!!",
headers={"WWW-Authenticate": "Bearer"}
)
finally:
self.conn.unbind()
def add_ou(self,ouName: str):
'''
作用:
创建ou
:param ouname:
:return:
'''
self.conn.add(
f'ou={ouName},{basednPeople["basedn"]}',
['organizationalUnit'],
attributes={'objectClass':['organizationalUnit']}
)
return self.conn.result
def add_users(self,cnName: str,userPassword: str,title: str,mail: EmailStr,displayName: str,departmentOu: str):
'''
作用:
新建用户
dn: 用户条目,departmentOu为部门组织,baseou为基础组织
objectclass: inetOrgPerson
departmentOu: 添加组织单位
cnName:用户名
:return:
'''
self.conn.add(
f'cn={cnName},ou={departmentOu},{basednPeople["basedn"]}',
['inetOrgPerson'],
{
'sn': cnName,
'mail': mail,
'cn': cnName,
'ou': departmentOu,
'userPassword': userPassword,
'title': title,
'uid': cnName,
'displayName': displayName
}
)
return self.conn.result
def add_group(self,groupName: str,cnName:str,ouName:str):
'''
作用:
创建Group
参数解释:
使用groupOfNames结构对象,需要member属性,以防止创建空组,member可以是新用户或已存在用户
groupName: 要创建的Group名称
ouName: 组织单位,非分组
cnName: 公共名称,你可以理解为用户名
:return:
'''
self.conn.add(
f'cn={groupName},{basednGroup["basedn"]}',
['groupOfNames'],
{
'cn': f'{groupName}',
'member': f'cn={cnName},ou={ouName},{basednPeople["basedn"]}'
}
)
return self.conn.result
def get_all_group(self):
'''
作用:
列出所有Group
参数解释:
attributes:ALL_ATTRIBUTES 获取所有字段
conn.bind(): 说明Connection连接成功,否则失败
返回值:
:return: 返回Dict
'''
try:
self.entryStatus = self.conn.search(
search_base=f'{basednGroup["basedn"]}',
search_filter="(objectClass=groupOfNames)",
attributes = ALL_ATTRIBUTES,
search_scope=SUBTREE,
)
entryGroupList = {}
for entry in self.conn.entries:
entryGroupList.update({json.loads(entry.entry_to_json())['attributes']['cn'][0]:json.loads(entry.entry_to_json())['attributes']})
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=e
)
finally:
self.conn.unbind
return entryGroupList
def get_all_users(self):
'''
作用:
获取所有用户
参数解释:
entry['cn']: 可以直接获取用户名
search_base: basedn 根目录,对应ldapsearch命令-b,dc=demo,dc=cn
attributes:ALL_ATTRIBUTES 返回所有字段
返回值:
:return: 返回Dict
'''
try:
self.conn.search(
search_base=f'{baseDn["basedn"]}',
search_filter="(objectClass=inetOrgPerson)",
attributes = ALL_ATTRIBUTES,
search_scope=SUBTREE,
)
entryUserList={}
for entry in self.conn.entries:
entryUserList.update({json.loads(entry.entry_to_json())['attributes']['cn'][0]:json.loads(entry.entry_to_json())['attributes']})
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=e
)
finally:
self.conn.unbind
return entryUserList
def get_all_ou(self):
'''
作用:
获取所有ou
参数解释:
unbind:断开链接
:return:
'''
try:
self.conn.search(
search_base=f'{baseDn["basedn"]}',
search_filter="(objectClass=organizationalUnit)",
attributes = ALL_ATTRIBUTES,
search_scope=SUBTREE,
)
entryOuList = {}
for entry in self.conn.entries:
entryOuList.update({json.loads(entry.entry_to_json())['attributes']['ou'][0]:json.loads(entry.entry_to_json())['attributes']})
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=e
)
finally:
self.conn.unbind()
return entryOuList
添加用户接口
每个接口都需要传入token: str = Depends(oauth2_scheme),这样如果要请求该接口,都需要身份验证。
ldap_api.LDAP() 传入token解密后的账号和密码,这边用的内部使用系统,对外的系统建议用jwt方式.
@router.post('/users/',description="添加用户")
async def add_users(cn: cnIn,token: str = Depends(oauth2_scheme)):
ldap = ldap_api.LDAP(Token.Decryption(token)["username"],Token.Decryption(token)["password"])
result = ldap.add_users(cn.cn,cn.userPassword,cn.title,cn.mail,cn.displayName,cn.departmentOu)
return result
用户删除
@router.delete("/users/{username}",description="用户删除")
async def delete_users(username: str,token: str = Depends(oauth2_scheme)):
ldap = ldap_api.LDAP(Token.Decryption(token)["username"],Token.Decryption(token)["password"])
result = ldap.delete_users(username)
return result
删除group
@router.delete("/group/{groupName}",description="删除Groups")
async def delete_groups(groupName: str,token: str = Depends(oauth2_scheme)):
ldap = ldap_api.LDAP(Token.Decryption(token)["username"],Token.Decryption(token)["password"])
result = ldap.delete_groups(groupName)
return result
修改用户密码
@router.put("/users/{username}/password",description="修改用户密码")
async def modify_password(username: str,new_password: str,token: str = Depends(oauth2_scheme)):
ldap = ldap_api.LDAP(Token.Decryption(token)["username"], Token.Decryption(token)["password"])
result = ldap.modify_password(username,new_password)
return result
效果图
打开交互式文档
http://127.0.0.1:8888/docs#/
点击右上角的Authorize,身份验证通过了才能请求接口
验证通过
添加Groups
groupName填入要新建的Group名称,创建Group时候,一定要传入member(用户)属性,防止创建空组。
result为0表示创建成功
删除Groups
创建用户
departmentOu为组织(ou)名称
删除用户
修改用户密码
普通用户操作其他用户权限?
用zhangsan登录
可以看到响应信息,普通用户是没有写入权限的
更多文章请扫一扫
扫描下面二维码关注公众号,获取更多学习资源