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
from datetime import datetime, timedelta
from typing import List, Union
from typing import List, Union, Optional
from jwt import PyJWTError
from fastapi import Depends, HTTPException, status, Security
@ -10,7 +10,7 @@ from sqlalchemy.orm import Session
from jose import jwt
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
@ -73,14 +73,36 @@ def get_user(username: str, id_: str, locked_out: bool, scopes: List[str]) -> Us
def authenticate_user(
username: str, password: str, db: Session
) -> Union[UserModel, bool]:
found, user = UserModel.auth(username, password, db)
if not found:
return False
username: str, password: str, client_id: Optional[int], otp: int, db: Session
) -> Optional[UserModel]:
user = UserModel.auth(username, password, db)
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(
security_scopes: SecurityScopes, token: str = Depends(oauth2_scheme),
) -> UserToken:

View File

@ -6,7 +6,7 @@ from datetime import datetime
from sqlalchemy.schema import ForeignKey, Table
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 .meta import Base
@ -46,14 +46,6 @@ class Client(Base):
)
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
def create(cls, dbsession):
client_code = random.randint(1000, 9999)
@ -114,16 +106,16 @@ class User(Base):
self.id = id_
@classmethod
def auth(cls, name, password, db) -> (bool, any):
def auth(cls, name, password, db) -> any:
if password is None:
return False, None
return None
user = db.query(User).filter(User.name.ilike(name)).first()
if not user:
return False, None
return None
if user.password != encrypt(password) or user.locked_out:
return False, None
return None
else:
return True, user
return user
class LoginHistory(Base):

View File

@ -1,13 +1,14 @@
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.responses import JSONResponse
from sqlalchemy.orm import Session
from ..core.security import (
Token,
authenticate_user,
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 ..schemas.auth import UserToken
@ -26,15 +27,31 @@ def get_db():
@router.post("/token", response_model=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:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
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 = create_access_token(
data={

View File

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

View File

@ -19,19 +19,13 @@
</mat-form-field>
</div>
<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">
<mat-form-field fxFlex>
<mat-label>Otp</mat-label>
<input matInput placeholder="Otp" formControlName="otp">
</mat-form-field>
</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>
</mat-card-content>
<mat-card-actions>

View File

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