关注公众号,将获取更多运维干货

实现流程

编写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,身份验证通过了才能请求接口

FastAPI开发OpenLDAP管理平台(二)_json

验证通过


FastAPI开发OpenLDAP管理平台(二)_sed_02



添加Groups

groupName填入要新建的Group名称,创建Group时候,一定要传入member(用户)属性,防止创建空组。

FastAPI开发OpenLDAP管理平台(二)_sed_03

result为0表示创建成功

FastAPI开发OpenLDAP管理平台(二)_json_04



删除Groups

FastAPI开发OpenLDAP管理平台(二)_sed_05

FastAPI开发OpenLDAP管理平台(二)_sed_06



创建用户

departmentOu为组织(ou)名称

FastAPI开发OpenLDAP管理平台(二)_sed_07


FastAPI开发OpenLDAP管理平台(二)_sed_08



删除用户

FastAPI开发OpenLDAP管理平台(二)_sed_09

FastAPI开发OpenLDAP管理平台(二)_json_10


修改用户密码

FastAPI开发OpenLDAP管理平台(二)_json_11

FastAPI开发OpenLDAP管理平台(二)_json_12



普通用户操作其他用户权限?

zhangsan登录

FastAPI开发OpenLDAP管理平台(二)_sed_13

可以看到响应信息,普通用户是没有写入权限的


FastAPI开发OpenLDAP管理平台(二)_sed_14

FastAPI开发OpenLDAP管理平台(二)_json_15



更多文章请扫一扫

 扫描下面二维码关注公众号获取更多学习资源

FastAPI开发OpenLDAP管理平台(二)_sed_16