Clients also implemented.

I think the only things left are the login history and other past errors
This commit is contained in:
Amritanshu Agrawal 2020-05-30 15:41:55 +05:30
parent 7edac38435
commit d5bc818632
7 changed files with 79 additions and 48 deletions

View File

@ -1,6 +1,6 @@
import uuid import uuid
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import List, Union from typing import List, Union, Optional
from jwt import PyJWTError from jwt import PyJWTError
from fastapi import Depends, HTTPException, status, Security from fastapi import Depends, HTTPException, status, Security
@ -10,7 +10,7 @@ from sqlalchemy.orm import Session
from jose import jwt from jose import jwt
from jose.exceptions import ExpiredSignatureError from jose.exceptions import ExpiredSignatureError
from brewman.models.auth import User as UserModel from brewman.models.auth import User as UserModel, Client
from ..db.session import SessionLocal from ..db.session import SessionLocal
@ -73,14 +73,36 @@ def get_user(username: str, id_: str, locked_out: bool, scopes: List[str]) -> Us
def authenticate_user( def authenticate_user(
username: str, password: str, db: Session username: str, password: str, client_id: Optional[int], otp: int, db: Session
) -> Union[UserModel, bool]: ) -> Optional[UserModel]:
found, user = UserModel.auth(username, password, db) user = UserModel.auth(username, password, db)
if not found:
return False
return user return user
def client_allowed(user: UserModel, client_id: int, otp: Optional[int] = None, db: Session = None) -> (bool, int):
client = db.query(Client).filter(Client.code == client_id).first() if client_id else None
allowed = "clients" in set(
[
p.name.replace(" ", "-").lower()
for r in user.roles
for p in r.permissions
]
)
if allowed:
return True, 0
elif client is None:
client = Client.create(db)
return False, client.code
elif client.enabled:
return True, client.code
elif client.otp == otp:
client.otp = None
client.enabled = True
return True, client.code
else:
return False, client.code
async def get_current_user( async def get_current_user(
security_scopes: SecurityScopes, token: str = Depends(oauth2_scheme), security_scopes: SecurityScopes, token: str = Depends(oauth2_scheme),
) -> UserToken: ) -> UserToken:

View File

@ -6,7 +6,7 @@ from datetime import datetime
from sqlalchemy.schema import ForeignKey, Table from sqlalchemy.schema import ForeignKey, Table
from sqlalchemy import Column, Boolean, Unicode, Integer, DateTime, UniqueConstraint from sqlalchemy import Column, Boolean, Unicode, Integer, DateTime, UniqueConstraint
from sqlalchemy.orm import synonym, relationship from sqlalchemy.orm import synonym, relationship, Session
from brewman.models.guidtype import GUID from brewman.models.guidtype import GUID
from .meta import Base from .meta import Base
@ -46,14 +46,6 @@ class Client(Base):
) )
self.id = id_ self.id = id_
@classmethod
def by_code(cls, code, dbsession):
if code is None:
return None
if not isinstance(code, int):
code = int(code)
return dbsession.query(cls).filter(cls.code == code).first()
@classmethod @classmethod
def create(cls, dbsession): def create(cls, dbsession):
client_code = random.randint(1000, 9999) client_code = random.randint(1000, 9999)
@ -114,16 +106,16 @@ class User(Base):
self.id = id_ self.id = id_
@classmethod @classmethod
def auth(cls, name, password, db) -> (bool, any): def auth(cls, name, password, db) -> any:
if password is None: if password is None:
return False, None return None
user = db.query(User).filter(User.name.ilike(name)).first() user = db.query(User).filter(User.name.ilike(name)).first()
if not user: if not user:
return False, None return None
if user.password != encrypt(password) or user.locked_out: if user.password != encrypt(password) or user.locked_out:
return False, None return None
else: else:
return True, user return user
class LoginHistory(Base): class LoginHistory(Base):

View File

@ -1,13 +1,14 @@
from datetime import timedelta from datetime import timedelta
from fastapi import APIRouter, Depends, HTTPException, status, Security from fastapi import APIRouter, Depends, HTTPException, status, Security, Cookie, Form, Response
from fastapi.security import OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordRequestForm
from fastapi.responses import JSONResponse
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from ..core.security import ( from ..core.security import (
Token, Token,
authenticate_user, authenticate_user,
ACCESS_TOKEN_EXPIRE_MINUTES, ACCESS_TOKEN_EXPIRE_MINUTES,
create_access_token, get_current_active_user, create_access_token, get_current_active_user, client_allowed,
) )
from ..db.session import SessionLocal from ..db.session import SessionLocal
from ..schemas.auth import UserToken from ..schemas.auth import UserToken
@ -26,15 +27,31 @@ def get_db():
@router.post("/token", response_model=Token) @router.post("/token", response_model=Token)
async def login_for_access_token( async def login_for_access_token(
form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db) response: Response,
form_data: OAuth2PasswordRequestForm = Depends(),
client_id: int = Cookie(None),
otp: int = Form(None),
db: Session = Depends(get_db),
): ):
user = authenticate_user(form_data.username, form_data.password, db) user = authenticate_user(form_data.username, form_data.password, client_id, otp, db)
if not user: if not user:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password", detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
allowed, c_id = client_allowed(user, client_id, otp, db)
db.commit()
if c_id and c_id != client_id:
response.set_cookie(key="client_id", value=str(c_id), max_age=10 * 365 * 24 * 60 * 60)
if not allowed:
not_allowed_response = JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
headers={"WWW-Authenticate": "Bearer"},
content={"detail": "Client is not registered"}
)
not_allowed_response.set_cookie(key="client_id", value=str(c_id), max_age=10 * 365 * 24 * 60 * 60)
return not_allowed_response
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token( access_token = create_access_token(
data={ data={

View File

@ -24,10 +24,11 @@ export class AuthService {
return this.currentUserSubject.value; return this.currentUserSubject.value;
} }
login(username: string, password: string) { login(username: string, password: string, otp: string) {
const formData: FormData = new FormData(); const formData: FormData = new FormData();
formData.append('username', username); formData.append('username', username);
formData.append('password', password); formData.append('password', password);
formData.append('otp', otp);
formData.append('grant_type', 'password'); formData.append('grant_type', 'password');
return this.http.post<any>(loginUrl, formData) return this.http.post<any>(loginUrl, formData)
.pipe(map(u => u.access_token)) .pipe(map(u => u.access_token))

View File

@ -19,19 +19,13 @@
</mat-form-field> </mat-form-field>
</div> </div>
<mat-divider></mat-divider> <mat-divider></mat-divider>
<h2 *ngIf="showOtp">Client ID: {{clientID}}</h2> <h2 *ngIf="showOtp">Client ID: {{clientId}}</h2>
<div fxLayout="row" fxLayout.lt-md="column" fxLayoutGap="20px" fxLayoutGap.lt-md="0px" *ngIf="showOtp"> <div fxLayout="row" fxLayout.lt-md="column" fxLayoutGap="20px" fxLayoutGap.lt-md="0px" *ngIf="showOtp">
<mat-form-field fxFlex> <mat-form-field fxFlex>
<mat-label>Otp</mat-label> <mat-label>Otp</mat-label>
<input matInput placeholder="Otp" formControlName="otp"> <input matInput placeholder="Otp" formControlName="otp">
</mat-form-field> </mat-form-field>
</div> </div>
<div fxLayout="row" fxLayout.lt-md="column" fxLayoutGap="20px" fxLayoutGap.lt-md="0px" *ngIf="showOtp">
<mat-form-field fxFlex>
<mat-label>Client Name</mat-label>
<input matInput placeholder="Client Name" formControlName="clientName">
</mat-form-field>
</div>
</form> </form>
</mat-card-content> </mat-card-content>
<mat-card-actions> <mat-card-actions>

View File

@ -15,7 +15,7 @@ export class LoginComponent implements OnInit, AfterViewInit {
form: FormGroup; form: FormGroup;
hide: boolean; hide: boolean;
showOtp: boolean; showOtp: boolean;
clientID: string; clientId: string;
private returnUrl: string; private returnUrl: string;
constructor(private route: ActivatedRoute, constructor(private route: ActivatedRoute,
@ -34,8 +34,7 @@ export class LoginComponent implements OnInit, AfterViewInit {
this.form = this.fb.group({ this.form = this.fb.group({
username: '', username: '',
password: '', password: '',
otp: '', otp: ''
clientName: ''
}); });
} }
@ -53,17 +52,20 @@ export class LoginComponent implements OnInit, AfterViewInit {
const formModel = this.form.value; const formModel = this.form.value;
const username = formModel.username; const username = formModel.username;
const password = formModel.password; const password = formModel.password;
this.auth.login(username, password).subscribe( const otp = formModel.otp;
(result) => { this.auth.login(username, password, otp)
this.router.navigateByUrl(this.returnUrl); // .pipe(first())
}, .subscribe(
(error) => { data => {
if (error.status === 403 && ['Unknown Client', 'OTP not supplied', 'OTP is wrong'].indexOf(error.error) !== -1) { this.router.navigate([this.returnUrl]);
this.showOtp = true; },
this.clientID = this.cs.getCookie('ClientID'); (error) => {
} if (error.status === 401 && 'Client is not registered' == error.error.detail) {
this.toaster.show('Danger', error.error); this.showOtp = true;
} this.clientId = this.cs.getCookie('client_id');
); }
this.toaster.show('Danger', error.error.details);
}
)
} }
} }

View File

@ -1,3 +1,4 @@
setuptools
wheel wheel
uvicorn uvicorn
fastapi fastapi
@ -10,3 +11,5 @@ python-multipart
pyjwt pyjwt
alembic alembic
itsdangerous itsdangerous
pydantic
starlette