Skip to content

Commit

Permalink
Merge pull request #33 from daystram/dev
Browse files Browse the repository at this point in the history
  • Loading branch information
daystram authored Jan 25, 2021
2 parents 2936d36 + 85214c3 commit 2f94993
Show file tree
Hide file tree
Showing 18 changed files with 309 additions and 112 deletions.
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[![Docker Pulls](https://img.shields.io/docker/pulls/daystram/ratify)](https://hub.docker.com/r/daystram/ratify)
[![MIT License](https://img.shields.io/github/license/daystram/ratify)](https://github.com/daystram/ratify/blob/master/LICENSE)

Ratify is a Central Authentication Service (CAS) implementing OAuth 2.0 and OpenID Connect (OIDC) protocols, as defined in [RFC 6749](https://tools.ietf.org/html/rfc6749).
Ratify is a Central Authentication Service (CAS) implementing OAuth 2.0 and OpenID Connect (OID) protocols, as defined in [RFC 6749](https://tools.ietf.org/html/rfc6749).

## Features
- Implements various authorization flows
Expand All @@ -14,7 +14,7 @@ Ratify is a Central Authentication Service (CAS) implementing OAuth 2.0 and Open
- Multi-factor authentication using Time-based One-Time Password (TOTP)
- Universal login
- User authentication and incident log
- _WIP: Active session management_
- Active session management

## Supported Authorizaton Flows
- Authorization Code
Expand Down Expand Up @@ -107,4 +107,3 @@ services:
## License
This project is licensed under the [MIT License](./LICENSE).
2 changes: 2 additions & 0 deletions ratify-be/constants/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package constants

const (
LogTypeLogin = "LOGN"
LogTypeUserAdmin = "USAD"
LogTypeUser = "USER"
LogTypeApplication = "APPN"

Expand All @@ -13,6 +14,7 @@ const (
LogScopeOAuthAuthorize = "oauth::authorize"
LogScopeUserProfile = "user::profile"
LogScopeUserPassword = "user::password"
LogScopeUserSuperuser = "user::superuser"
LogScopeUserSession = "user::session"
LogScopeUserMFA = "user::mfa"
LogScopeApplicationDetail = "application::detail"
Expand Down
8 changes: 5 additions & 3 deletions ratify-be/controllers/v1/dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@ func GETDashboardInfo(c *gin.Context) {
return
}
dashboardInfo := datatransfers.DashboardInfo{
SignInCount: user.LoginCount,
LastSignIn: user.LastLogin,
SessionCount: len(activeSessions),
SignInCount: user.LoginCount,
LastSignIn: user.LastLogin,
SessionCount: len(activeSessions),
MFAEnabled: user.EnabledTOTP(),
RecentFailure: user.RecentFailure,
}
c.JSON(http.StatusOK, datatransfers.APIResponse{Data: dashboardInfo})
return
Expand Down
53 changes: 52 additions & 1 deletion ratify-be/controllers/v1/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ func GETUserDetail(c *gin.Context) {
GivenName: user.GivenName,
FamilyName: user.FamilyName,
Subject: user.Subject,
Superuser: user.Superuser,
Username: user.Username,
Email: user.Email,
EmailVerified: user.EmailVerified,
Expand Down Expand Up @@ -150,7 +151,7 @@ func PUTUser(c *gin.Context) {
// @Tags user
// @Param user body datatransfers.UserUpdatePassword true "User update password info"
// @Success 200 "OK"
// @Router /api/v1/user/password [POST]
// @Router /api/v1/user/password [PUT]
func PUTUserPassword(c *gin.Context) {
var err error
// fetch verification info
Expand Down Expand Up @@ -183,6 +184,56 @@ func PUTUserPassword(c *gin.Context) {
return
}

// @Summary Update user superuser status
// @Tags user
// @Param user body datatransfers.UserUpdateSuperuser true "User update superuser info"
// @Success 200 "OK"
// @Router /api/v1/user/superuser [PUT]
func PUTUserSuperuser(c *gin.Context) {
var err error
// fetch superuser info
var superuser datatransfers.UserUpdateSuperuser
if err = c.ShouldBindJSON(&superuser); err != nil {
c.JSON(http.StatusBadRequest, datatransfers.APIResponse{Error: err.Error()})
return
}
// prevent changing own status
if c.GetString(constants.UserSubjectKey) == superuser.Subject {
c.JSON(http.StatusBadRequest, datatransfers.APIResponse{Error: "cannot change own superuser status"})
return
}
// retrieve users
var user, target models.User
if user, err = handlers.Handler.UserGetOneBySubject(c.GetString(constants.UserSubjectKey)); err != nil {
c.JSON(http.StatusNotFound, datatransfers.APIResponse{Error: "user not found"})
return
}
if target, err = handlers.Handler.UserGetOneBySubject(superuser.Subject); err != nil {
c.JSON(http.StatusNotFound, datatransfers.APIResponse{Error: "user not found"})
return
}
// clear session
var activeSessions []*datatransfers.SessionInfo
if activeSessions, err = handlers.Handler.SessionGetAllActive(superuser.Subject); err != nil {
c.JSON(http.StatusInternalServerError, datatransfers.APIResponse{Error: "cannot retrieve active sessions"})
return
}
for _, session := range activeSessions {
handlers.Handler.SessionRevoke(session.SessionID)
}
// update status
if err = handlers.Handler.UserUpdateSuperuser(superuser.Subject, superuser.Superuser); err != nil {
c.JSON(http.StatusInternalServerError, datatransfers.APIResponse{Error: "failed updating user"})
return
}
handlers.Handler.LogInsertUser(user, superuser.Superuser, datatransfers.LogDetail{
Scope: constants.LogScopeUserSuperuser,
Detail: datatransfers.UserInfo{Username: target.Username},
})
c.JSON(http.StatusOK, datatransfers.APIResponse{})
return
}

// @Summary Verify user email
// @Tags user
// @Param user body datatransfers.UserVerify true "User verification info"
Expand Down
8 changes: 5 additions & 3 deletions ratify-be/datatransfers/dashboard.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package datatransfers

type DashboardInfo struct {
SignInCount int `json:"signin_count"`
LastSignIn int64 `json:"last_signin"`
SessionCount int `json:"session_count"`
SignInCount int `json:"signin_count"`
LastSignIn int64 `json:"last_signin"`
SessionCount int `json:"session_count"`
MFAEnabled bool `json:"mfa_enabled"`
RecentFailure bool `json:"recent_failure"`
}
6 changes: 6 additions & 0 deletions ratify-be/datatransfers/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ type UserUpdatePassword struct {
New string `json:"new_password" binding:"required"`
}

type UserUpdateSuperuser struct {
Subject string `json:"sub" binding:"required"`
Superuser bool `json:"superuser"`
}

type UserVerify struct {
Token string `json:"token" binding:"required"`
}
Expand All @@ -38,6 +43,7 @@ type UserInfo struct {
FamilyName string `json:"family_name"`
Subject string `json:"sub"`
Username string `uri:"preferred_username" json:"preferred_username"`
Superuser bool `json:"superuser"`
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
MFAEnabled bool `json:"mfa_enabled"`
Expand Down
1 change: 1 addition & 0 deletions ratify-be/handlers/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type handlerFunc interface {
UserGetAll() (users []models.User, err error)
UserUpdate(subject string, user datatransfers.UserUpdate) (err error)
UserUpdatePassword(subject, oldPassword, newPassword string) (err error)
UserUpdateSuperuser(subject string, superuser bool) (err error)

// application
ApplicationGetOneByClientID(clientID string) (application models.Application, err error)
Expand Down
14 changes: 12 additions & 2 deletions ratify-be/handlers/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ func (m *module) LogGetAllActivity(subject string) (logs []models.Log, err error
if logs, err = m.db.logOrmer.GetAllByUserSubject(subject); err != nil {
return nil, fmt.Errorf("cannot retrieve logs. %+v", err)
}
m.db.userOrmer.FlagRecentFailure(models.User{Subject: subject}, false)
return
}

Expand All @@ -27,7 +28,11 @@ func (m *module) LogGetAllAdmin() (logs []models.Log, err error) {

func (m *module) LogInsertLogin(user models.User, application models.Application, success bool, detail datatransfers.LogDetail) {
description, _ := json.Marshal(detail)
m.db.userOrmer.IncrementLoginCount(user)
if success {
m.db.userOrmer.IncrementLoginCount(user)
} else {
m.db.userOrmer.FlagRecentFailure(user, true)
}
m.logEntry(models.Log{
UserSubject: sql.NullString{String: user.Subject, Valid: true},
ApplicationClientID: sql.NullString{String: application.ClientID, Valid: true},
Expand All @@ -43,9 +48,14 @@ func (m *module) LogInsertAuthorize(application models.Application, _ bool, _ da

func (m *module) LogInsertUser(user models.User, success bool, detail datatransfers.LogDetail) {
description, _ := json.Marshal(detail)
logType := constants.LogTypeUser
if detail.Scope == constants.LogScopeUserSuperuser {
logType = constants.LogTypeUserAdmin
}
log.Println(logType)
m.logEntry(models.Log{
UserSubject: sql.NullString{String: user.Subject, Valid: true},
Type: constants.LogTypeUser,
Type: logType,
Severity: map[bool]string{true: constants.LogSeverityInfo, false: constants.LogSeverityWarn}[success],
Description: string(description),
})
Expand Down
10 changes: 10 additions & 0 deletions ratify-be/handlers/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,13 @@ func (m *module) UserUpdatePassword(subject, oldPassword, newPassword string) (e
}
return
}

func (m *module) UserUpdateSuperuser(subject string, superuser bool) (err error) {
if err = m.db.userOrmer.UpdateUserSuperuser(models.User{
Subject: subject,
Superuser: superuser,
}); err != nil {
return errors.New("cannot update user superuser status")
}
return
}
3 changes: 2 additions & 1 deletion ratify-be/models/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package models

import (
"database/sql"

"gorm.io/gorm"

"github.com/daystram/ratify/ratify-be/constants"
Expand Down Expand Up @@ -56,7 +57,7 @@ func (o *logOrm) GetAllActivityByApplicationClientID(applicationClientID string)

func (o *logOrm) GetAllAdmin() (logs []Log, err error) {
result := o.db.Model(&Log{}).
Where("type = ?", constants.LogTypeApplication).
Where("type = ? OR type = ?", constants.LogTypeApplication, constants.LogTypeUserAdmin).
Preload("User").Preload("Application").
Order("created_at DESC").
Find(&logs)
Expand Down
22 changes: 21 additions & 1 deletion ratify-be/models/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type User struct {
UpdatedAt int64 `gorm:"column:updated_at;autoUpdateTime" json:"-"`
LastLogin int64 `gorm:"column:last_login;default:0" json:"-"`
LoginCount int `gorm:"column:login_count;default:0" json:"-"`
RecentFailure bool `gorm:"column:recent_failure;default:false" json:"-"`
}

type UserOrmer interface {
Expand All @@ -36,11 +37,13 @@ type UserOrmer interface {
GetAll() (users []User, err error)
InsertUser(user User) (subject string, err error)
UpdateUser(user User) (err error)
UpdateUserSuperuser(user User) (err error)
FlagRecentFailure(user User, failed bool) (err error)
IncrementLoginCount(user User) (err error)
}

func NewUserOrmer(db *gorm.DB) UserOrmer {
_ = db.AutoMigrate(&User{}) // builds table when enabled
// _ = db.AutoMigrate(&User{}) // builds table when enabled
return &userOrm{db}
}

Expand Down Expand Up @@ -75,6 +78,23 @@ func (o *userOrm) UpdateUser(user User) (err error) {
return result.Error
}

func (o *userOrm) UpdateUserSuperuser(user User) (err error) {
// required due to UpdateUser() treats false bool as empty field when not using sql.NullBool
result := o.db.Model(&User{}).Where("sub = ?", user.Subject).
Updates(map[string]interface{}{
"superuser": gorm.Expr("?", user.Superuser),
})
return result.Error
}

func (o *userOrm) FlagRecentFailure(user User, failed bool) (err error) {
result := o.db.Model(&User{}).Where("sub = ?", user.Subject).
Updates(map[string]interface{}{
"recent_failure": gorm.Expr("?", failed),
})
return result.Error
}

func (o *userOrm) IncrementLoginCount(user User) (err error) {
result := o.db.Model(&User{}).Where("sub = ?", user.Subject).
Updates(map[string]interface{}{
Expand Down
1 change: 1 addition & 0 deletions ratify-be/router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ func InitializeRouter() (router *gin.Engine) {
user.POST("/", v1.POSTRegister)
user.PUT("/", utils.AuthOnly, v1.PUTUser)
user.PUT("/password", utils.AuthOnly, v1.PUTUserPassword)
user.PUT("/superuser", utils.AuthOnly, utils.SuperuserOnly, v1.PUTUserSuperuser)
user.POST("/verify", v1.POSTVerify)
user.POST("/resend", v1.POSTResend)
}
Expand Down
3 changes: 3 additions & 0 deletions ratify-fe/src/apis/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ export default {
updatePassword: function(passwords: object): Promise<AxiosResponse> {
return apiClient.put(`user/password`, passwords, withAuth());
},
updateSuperuser: function(superuser: object): Promise<AxiosResponse> {
return apiClient.put(`user/superuser`, superuser, withAuth());
},
signup: function(userSignup: object): Promise<AxiosResponse> {
return apiClient.post(`user/`, userSignup);
},
Expand Down
75 changes: 74 additions & 1 deletion ratify-fe/src/views/manage/Dashboard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,55 @@
</v-card>
</v-col>
</v-row>
<v-row>
<v-col cols="12">
<v-card>
<v-card-title>
<v-row no-gutters align="center">
<v-col cols="auto">
Updates
</v-col>
</v-row>
</v-card-title>
<v-divider inset />
<div class="v-card__body">
<v-expand-transition>
<v-row v-if="activity.formLoadStatus === STATUS.COMPLETE">
<v-col v-if="!this.updates.length">
<div class="text--disabled font-italic text-center">
You're all set!
</div>
</v-col>
<div
v-for="(update, i) in updates"
:key="i"
style="width: 100%"
>
<v-col>
<v-alert
:type="update.severity"
text
prominent
dense
:icon="update.icon"
transition="scroll-y-transition"
class="ma-0"
>
<h3 class="text-h6">
{{ update.title }}
</h3>
<div>
{{ update.detail }}
</div>
</v-alert>
</v-col>
</div>
</v-row>
</v-expand-transition>
</div>
</v-card>
</v-col>
</v-row>
</div>
</template>

Expand All @@ -86,7 +135,13 @@ export default Vue.extend({
signInCount: 0,
lastSignIn: new Date(),
sessionCount: 0
}
},
updates: new Array<{
severity: string;
title: string;
detail: string;
icon: string;
}>()
};
},
Expand All @@ -104,6 +159,24 @@ export default Vue.extend({
response.data.data.last_signin * 1000
);
this.activity.sessionCount = response.data.data.session_count;
if (response.data.data.recent_failure) {
this.updates.push({
severity: "error",
title: "Failed Sign In Attempt",
detail:
"There has been a recent failed sign in attempt to your account. Go to Activities page to view more.",
icon: "mdi-account-alert"
});
}
if (!response.data.data.mfa_enabled) {
this.updates.push({
severity: "warning",
title: "MFA Disabled",
detail:
"Multi-factor authentication is not enabled. Enable in your Profile page to ehance your account security.",
icon: "mdi-two-factor-authentication"
});
}
})
.catch(() => {
this.activity.formLoadStatus = STATUS.ERROR;
Expand Down
Loading

0 comments on commit 2f94993

Please sign in to comment.