Skip to content

Commit

Permalink
feat(back): Role based access
Browse files Browse the repository at this point in the history
- Add first admin email
- User endpoints (list, deactivate, activate)
- Move the storage of token from db to mem cache

Closes #45 #37 #40 #41
  • Loading branch information
michaelcoll committed May 21, 2023
1 parent 4495fab commit dcd9f21
Show file tree
Hide file tree
Showing 35 changed files with 612 additions and 467 deletions.
10 changes: 8 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,16 @@ jobs:
- name: Set APP_VERSION env
run: echo APP_VERSION=$(echo ${GITHUB_REF} | rev | cut -d'/' -f 1 | rev ) >> ${GITHUB_ENV}

- name: Set up Node.js
uses: actions/setup-node@v3
- uses: pnpm/action-setup@v2
with:
version: 8

- uses: actions/setup-node@v3
with:
node-version: 18.x
cache: 'pnpm'
cache-dependency-path: |
internal/web/pnpm-lock.yaml
- name: Prepare Vue App
run: |
Expand Down
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ WORKDIR /go/src/app
COPY . .

RUN go mod download
RUN CGO_ENABLED=0 go build -o /go/bin/quiz-app -ldflags="-s -w -X 'github.com/michaelcoll/quiz-app/cmd.version=$VERSION'"
RUN go build -o /go/bin/quiz-app -ldflags="-s -w -X 'github.com/michaelcoll/quiz-app/cmd.version=$VERSION'"

# Now copy it into our base image.
FROM gcr.io/distroless/static-debian11:nonroot
FROM gcr.io/distroless/base-debian11:nonroot

COPY --from=build-go /go/bin/quiz-app /bin/quiz-app

Expand Down
10 changes: 6 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ build-web:
&& pnpm run build

build-docker:
docker build . -t web --pull --build-arg VERSION=v0.0.1
docker build . -t michaelcoll/quiz-app:latest --pull --build-arg VERSION=v0.0.1

.PHONY: test
test:
Expand All @@ -43,7 +43,9 @@ vue-lint:
run-docker:
docker run -ti --rm -p 8080:8080 web:latest

.PHONY: sqlc
sqlc:
sqlc generate \
.PHONY: generate
generate:
go generate internal/back/domain/repositories.go \
&& go generate internal/back/domain/callers.go \
&& sqlc generate \
&& sqlc-addon generate --quiet
2 changes: 1 addition & 1 deletion cmd/banner.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const (
Serve Mode = 0
)

func Print(version string, mode Mode) {
func printBanner(version string, mode Mode) {
var modeStr string

switch mode {
Expand Down
12 changes: 7 additions & 5 deletions cmd/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Starts the server`,
}

func serve(_ *cobra.Command, _ []string) {
Print(version, Serve)
printBanner(version, Serve)

module := back.New()

Expand All @@ -52,10 +52,12 @@ func serve(_ *cobra.Command, _ []string) {
}

func init() {
serveCmd.Flags().StringP("repository-url", "r", "", "The url of the repository containing the quizzes")
serveCmd.Flags().StringP("token", "t", "", "The P.A.T. used to access the repository")
serveCmd.Flags().String("auth0-audience", "", "The Auth0 audience used in the clientId")
serveCmd.Flags().String("restrict-email-domain", "", "New users will have to be in this domain to be created")
serveCmd.Flags().StringP("repository-url", "r", "", "The url of the repository containing the quizzes.")
serveCmd.Flags().StringP("token", "t", "", "The P.A.T. used to access the repository.")
serveCmd.Flags().String("auth0-audience", "", "The Auth0 audience used in the clientId.")
serveCmd.Flags().String("restrict-email-domain", "", "New users will have to be in this domain to be created.")
serveCmd.Flags().String("default-admin-email", "",
"The default admin email. If specified when the user with the given email registers, it will be created with admin role automatically.")

_ = viper.BindPFlag("repository-url", serveCmd.Flags().Lookup("repository-url"))
_ = viper.BindPFlag("token", serveCmd.Flags().Lookup("token"))
Expand Down
2 changes: 1 addition & 1 deletion db/migrations/v1_init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ CREATE TABLE quiz
filename TEXT NOT NULL,
version INTEGER NOT NULL DEFAULT 1,
active INTEGER NOT NULL DEFAULT 1,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_at TEXT NOT NULL,

CONSTRAINT filename_fk FOREIGN KEY (filename) REFERENCES quiz (filename),
CONSTRAINT quiz_version_unique UNIQUE (filename, version)
Expand Down
21 changes: 2 additions & 19 deletions db/migrations/v2_auth.sql
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,8 @@ CREATE TABLE user
email TEXT NOT NULL,
firstname TEXT NOT NULL,
lastname TEXT NOT NULL,
active INTEGER NOT NULL DEFAULT 1
);

CREATE TABLE user_role
(
user_id TEXT,
role_id INTEGER,
active INTEGER NOT NULL DEFAULT 1,
role_id INTEGER NOT NULL,

CONSTRAINT pk PRIMARY KEY (user_id, role_id),
CONSTRAINT user_fk FOREIGN KEY (user_id) REFERENCES user (id),
CONSTRAINT role_fk FOREIGN KEY (role_id) REFERENCES role (id)
);

CREATE TABLE token
(
opaque_token TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
expires TIMESTAMP NOT NULL,
aud TEXT NOT NULL,

CONSTRAINT user_fk FOREIGN KEY (user_id) REFERENCES user (id)
);
38 changes: 17 additions & 21 deletions db/queries/auth.sql
Original file line number Diff line number Diff line change
@@ -1,26 +1,22 @@
-- name: FindUserById :many
SELECT u.*, ur.role_id
FROM user u LEFT JOIN user_role ur ON u.id = ur.user_id
-- name: FindUserById :one
SELECT *
FROM user
WHERE id = ?;

-- name: CreateOrReplaceUser :exec
REPLACE INTO user (id, email, firstname, lastname)
VALUES (?, ?, ?, ?);

-- name: FindTokenByTokenStr :one
SELECT t.*, u.email
FROM token t JOIN user u ON u.id = t.user_id
WHERE opaque_token = ?;

-- name: CreateOrReplaceToken :exec
REPLACE INTO token (opaque_token, user_id, expires, aud)
VALUES (?, ?, ?, ?);
-- name: FindAllUser :many
SELECT *
FROM user;

-- name: AddRoleToUser :exec
REPLACE INTO user_role (user_id, role_id)
VALUES (?, ?);
-- name: CreateOrReplaceUser :exec
REPLACE INTO user (id, email, firstname, lastname, role_id)
VALUES (?, ?, ?, ?, ?);

-- name: RemoveAllRoleFromUser :exec
DELETE FROM user_role
WHERE user_id = ?
-- name: UpdateUserRole :exec
UPDATE user
SET role_id = ?
WHERE id = ?;

-- name: UpdateUserActive :exec
UPDATE user
SET active = ?
WHERE id = ?;
4 changes: 2 additions & 2 deletions db/queries/quiz.sql
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
-- name: CreateOrReplaceQuiz :exec
REPLACE INTO quiz (sha1, name, filename, version)
VALUES (?, ?, ?, ?);
REPLACE INTO quiz (sha1, name, filename, version, created_at)
VALUES (?, ?, ?, ?, ?);

-- name: CreateOrReplaceQuestion :exec
REPLACE INTO quiz_question (sha1, content)
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
github.com/go-git/go-git/v5 v5.6.1
github.com/joeig/gin-cachecontrol v1.1.1
github.com/mattn/go-sqlite3 v1.14.16
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/spf13/cobra v1.7.0
github.com/spf13/viper v1.15.0
github.com/stretchr/testify v1.8.3
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
github.com/pelletier/go-toml/v2 v2.0.7 h1:muncTPStnKRos5dpVKULv2FVd4bMOhNePj9CjgDb8Us=
github.com/pelletier/go-toml/v2 v2.0.7/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
Expand Down
65 changes: 52 additions & 13 deletions internal/back/domain/authService.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ package domain

import (
"context"
"fmt"
"strings"
"time"

"github.com/fatih/color"
"github.com/spf13/viper"
)

Expand All @@ -30,44 +32,52 @@ type AuthService struct {
}

func NewAuthService(r AuthRepository, c AccessTokenCaller) AuthService {
adminEmail := viper.GetString("default-admin-email")

if len(adminEmail) > 0 {
fmt.Printf("%s Default admin email set to %s\n", color.GreenString("✓"), color.BlueString(adminEmail))
}

return AuthService{r: r, c: c}
}

func (s *AuthService) Register(ctx context.Context, user *User, accessToken string) error {
func (s *AuthService) Register(ctx context.Context, user *User, accessToken string) (*User, error) {
token, err := s.validateToken(ctx, accessToken)
if err != nil {
return err
return nil, err
}

if token.Email != user.Email {
return Errorf(UnAuthorized, "token email and user email mismatch (%s != %s)", token.Email, user.Email)
return nil, Errorf(UnAuthorized, "token email and user email mismatch (%s != %s)", token.Email, user.Email)
}

if token.Email != user.Email {
return Errorf(UnAuthorized, "token email and user email mismatch (%s != %s)", token.Email, user.Email)
return nil, Errorf(UnAuthorized, "token email and user email mismatch (%s != %s)", token.Email, user.Email)
}

if token.Sub != user.Id {
return Errorf(UnAuthorized, "token sub and user id mismatch (%s != %s)", token.Sub, user.Id)
return nil, Errorf(UnAuthorized, "token sub and user id mismatch (%s != %s)", token.Sub, user.Id)
}

emailDomain := strings.Split(user.Email, "@")[1]
restrictedDomainName := viper.GetString("restrict-email-domain")
if len(restrictedDomainName) > 0 && emailDomain != restrictedDomainName {
return Errorf(UnAuthorized, "user is not in a valid domain (%s not in domain %s)", user.Email, restrictedDomainName)
return nil, Errorf(UnAuthorized, "user is not in a valid domain (%s not in domain %s)", user.Email, restrictedDomainName)
}

err = s.r.CreateUser(ctx, user)
if err != nil {
return err
adminEmail := viper.GetString("default-admin-email")
if user.Email == adminEmail {
user.Role = Admin
} else {
user.Role = Student
}

err = s.r.AddRoleToUser(ctx, user.Id, Student)
err = s.r.CreateUser(ctx, user)
if err != nil {
return err
return nil, err
}

return nil
return user, nil
}

func (s *AuthService) validateToken(ctx context.Context, tokenStr string) (token *AccessToken, err error) {
Expand Down Expand Up @@ -114,7 +124,7 @@ func (s *AuthService) ValidateTokenAndGetUser(ctx context.Context, accessToken s
}

if token.Provenance == Api {
err := s.r.CreateToken(ctx, token)
err := s.r.CacheToken(ctx, token)
if err != nil {
return nil, err
}
Expand All @@ -135,3 +145,32 @@ func (s *AuthService) FindUserById(ctx context.Context, id string) (*User, error

return user, nil
}

func (role Role) CanAccess(other Role) bool {

if role == other {
return true
}

if role == Admin && (other == Teacher || other == Student) {
return true
}

if role == Teacher && other == Student {
return true
}

return false
}

func (s *AuthService) FindAllUser(ctx context.Context) ([]*User, error) {
return s.r.FindAllUser(ctx)
}

func (s *AuthService) DeactivateUser(ctx context.Context, id string) error {
return s.r.UpdateUserActive(ctx, id, false)
}

func (s *AuthService) ActivateUser(ctx context.Context, id string) error {
return s.r.UpdateUserActive(ctx, id, true)
}
10 changes: 10 additions & 0 deletions internal/back/domain/authService_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,13 @@ func TestAuthService_FindUserById(t *testing.T) {
}
}
}

func TestRole_canAccess(t *testing.T) {

assert.True(t, Admin.CanAccess(Teacher))
assert.True(t, Admin.CanAccess(Student))
assert.True(t, Teacher.CanAccess(Student))
assert.False(t, Teacher.CanAccess(Admin))
assert.False(t, Student.CanAccess(Admin))

}
Loading

0 comments on commit dcd9f21

Please sign in to comment.