From 89957e70586580c889cdfd18116c46abeda5caf9 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Tue, 25 Jan 2022 22:30:53 -0500 Subject: [PATCH] Docblocking --- Makefile | 2 +- auth/auth.go | 43 +++++++++++++++++++++++++++++++++++++++++-- auth/auth_sqlite.go | 41 ++++++++++++++++++++++++++++++++--------- 3 files changed, 74 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index fab92649..a9a9a201 100644 --- a/Makefile +++ b/Makefile @@ -80,7 +80,7 @@ vet: go vet ./... lint: - which golint || go get -u golang.org/x/lint/golint + which golint || go install golang.org/x/lint/golint@latest go list ./... | grep -v /vendor/ | xargs -L1 golint -set_exit_status staticcheck: .PHONY diff --git a/auth/auth.go b/auth/auth.go index 16a41670..377b54ce 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -4,22 +4,53 @@ import "errors" // Auther is a generic interface to implement password-based authentication and authorization type Auther interface { - Authenticate(user, pass string) (*User, error) + // Authenticate checks username and password and returns a user if correct. The method + // returns in constant-ish time, regardless of whether the user exists or the password is + // correct or incorrect. + Authenticate(username, password string) (*User, error) + + // Authorize returns nil if the given user has access to the given topic using the desired + // permission. The user param may be nil to signal an anonymous user. Authorize(user *User, topic string, perm Permission) error } +// Manager is an interface representing user and access management type Manager interface { + // AddUser adds a user with the given username, password and role. The password should be hashed + // before it is stored in a persistence layer. AddUser(username, password string, role Role) error + + // RemoveUser deletes the user with the given username. The function returns nil on success, even + // if the user did not exist in the first place. RemoveUser(username string) error + + // Users returns a list of users. It always also returns the Everyone user ("*"). Users() ([]*User, error) + + // User returns the user with the given username if it exists, or ErrNotFound otherwise. + // You may also pass Everyone to retrieve the anonymous user and its Grant list. User(username string) (*User, error) + + // ChangePassword changes a user's password ChangePassword(username, password string) error + + // ChangeRole changes a user's role. When a role is changed from RoleUser to RoleAdmin, + // all existing access control entries (Grant) are removed, since they are no longer needed. ChangeRole(username string, role Role) error - DefaultAccess() (read bool, write bool) + + // AllowAccess adds or updates an entry in th access control list for a specific user. It controls + // read/write access to a topic. AllowAccess(username string, topic string, read bool, write bool) error + + // ResetAccess removes an access control list entry for a specific username/topic, or (if topic is + // empty) for an entire user. ResetAccess(username string, topic string) error + + // DefaultAccess returns the default read/write access if no access control entry matches + DefaultAccess() (read bool, write bool) } +// User is a struct that represents a user type User struct { Name string Hash string // password hash (bcrypt) @@ -27,35 +58,43 @@ type User struct { Grants []Grant } +// Grant is a struct that represents an access control entry to a topic type Grant struct { Topic string Read bool Write bool } +// Permission represents a read or write permission to a topic type Permission int +// Permissions to a topic const ( PermissionRead = Permission(1) PermissionWrite = Permission(2) ) +// Role represents a user's role, either admin or regular user type Role string +// User roles const ( RoleAdmin = Role("admin") RoleUser = Role("user") RoleAnonymous = Role("anonymous") ) +// Everyone is a special username representing anonymous users const ( Everyone = "*" ) +// AllowedRole returns true if the given role can be used for new users func AllowedRole(role Role) bool { return role == RoleUser || role == RoleAdmin } +// Error constants used by the package var ( ErrUnauthenticated = errors.New("unauthenticated") ErrUnauthorized = errors.New("unauthorized") diff --git a/auth/auth_sqlite.go b/auth/auth_sqlite.go index f9794b94..ca0d4cd0 100644 --- a/auth/auth_sqlite.go +++ b/auth/auth_sqlite.go @@ -61,6 +61,8 @@ const ( deleteTopicAccessQuery = `DELETE FROM access WHERE user = ? AND topic = ?` ) +// SQLiteAuth is an implementation of Auther and Manager. It stores users and access control list +// in a SQLite database. type SQLiteAuth struct { db *sql.DB defaultRead bool @@ -74,6 +76,7 @@ var ( var _ Auther = (*SQLiteAuth)(nil) var _ Manager = (*SQLiteAuth)(nil) +// NewSQLiteAuth creates a new SQLiteAuth instance func NewSQLiteAuth(filename string, defaultRead, defaultWrite bool) (*SQLiteAuth, error) { db, err := sql.Open("sqlite3", filename) if err != nil { @@ -97,6 +100,9 @@ func setupNewAuthDB(db *sql.DB) error { return nil } +// Authenticate checks username and password and returns a user if correct. The method +// returns in constant-ish time, regardless of whether the user exists or the password is +// correct or incorrect. func (a *SQLiteAuth) Authenticate(username, password string) (*User, error) { if username == Everyone { return nil, ErrUnauthenticated @@ -113,17 +119,19 @@ func (a *SQLiteAuth) Authenticate(username, password string) (*User, error) { return user, nil } +// Authorize returns nil if the given user has access to the given topic using the desired +// permission. The user param may be nil to signal an anonymous user. func (a *SQLiteAuth) Authorize(user *User, topic string, perm Permission) error { if user != nil && user.Role == RoleAdmin { return nil // Admin can do everything } - // Select the read/write permissions for this user/topic combo. The query may return two - // rows (one for everyone, and one for the user), but prioritizes the user. The value for - // user.Name may be empty (= everyone). username := Everyone if user != nil { username = user.Name } + // Select the read/write permissions for this user/topic combo. The query may return two + // rows (one for everyone, and one for the user), but prioritizes the user. The value for + // user.Name may be empty (= everyone). rows, err := a.db.Query(selectTopicPermsQuery, username, topic) if err != nil { return err @@ -150,8 +158,10 @@ func (a *SQLiteAuth) resolvePerms(read, write bool, perm Permission) error { return ErrUnauthorized } +// AddUser adds a user with the given username, password and role. The password should be hashed +// before it is stored in a persistence layer. func (a *SQLiteAuth) AddUser(username, password string, role Role) error { - if !allowedUsernameRegex.MatchString(username) || (role != RoleAdmin && role != RoleUser) { + if !allowedUsernameRegex.MatchString(username) || !AllowedRole(role) { return ErrInvalidArgument } hash, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost) @@ -164,6 +174,8 @@ func (a *SQLiteAuth) AddUser(username, password string, role Role) error { return nil } +// RemoveUser deletes the user with the given username. The function returns nil on success, even +// if the user did not exist in the first place. func (a *SQLiteAuth) RemoveUser(username string) error { if !allowedUsernameRegex.MatchString(username) || username == Everyone { return ErrInvalidArgument @@ -177,6 +189,7 @@ func (a *SQLiteAuth) RemoveUser(username string) error { return nil } +// Users returns a list of users. It always also returns the Everyone user ("*"). func (a *SQLiteAuth) Users() ([]*User, error) { rows, err := a.db.Query(selectUsernamesQuery) if err != nil { @@ -210,6 +223,8 @@ func (a *SQLiteAuth) Users() ([]*User, error) { return users, nil } +// User returns the user with the given username if it exists, or ErrNotFound otherwise. +// You may also pass Everyone to retrieve the anonymous user and its Grant list. func (a *SQLiteAuth) User(username string) (*User, error) { if username == Everyone { return a.everyoneUser() @@ -277,6 +292,7 @@ func (a *SQLiteAuth) readGrants(username string) ([]Grant, error) { return grants, nil } +// ChangePassword changes a user's password func (a *SQLiteAuth) ChangePassword(username, password string) error { hash, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost) if err != nil { @@ -288,8 +304,10 @@ func (a *SQLiteAuth) ChangePassword(username, password string) error { return nil } +// ChangeRole changes a user's role. When a role is changed from RoleUser to RoleAdmin, +// all existing access control entries (Grant) are removed, since they are no longer needed. func (a *SQLiteAuth) ChangeRole(username string, role Role) error { - if !allowedUsernameRegex.MatchString(username) || (role != RoleAdmin && role != RoleUser) { + if !allowedUsernameRegex.MatchString(username) || !AllowedRole(role) { return ErrInvalidArgument } if _, err := a.db.Exec(updateUserRoleQuery, string(role), username); err != nil { @@ -303,10 +321,8 @@ func (a *SQLiteAuth) ChangeRole(username string, role Role) error { return nil } -func (a *SQLiteAuth) DefaultAccess() (read bool, write bool) { - return a.defaultRead, a.defaultWrite -} - +// AllowAccess adds or updates an entry in th access control list for a specific user. It controls +// read/write access to a topic. func (a *SQLiteAuth) AllowAccess(username string, topic string, read bool, write bool) error { if _, err := a.db.Exec(upsertUserAccessQuery, username, topic, read, write); err != nil { return err @@ -314,6 +330,8 @@ func (a *SQLiteAuth) AllowAccess(username string, topic string, read bool, write return nil } +// ResetAccess removes an access control list entry for a specific username/topic, or (if topic is +// empty) for an entire user. func (a *SQLiteAuth) ResetAccess(username string, topic string) error { if username == "" && topic == "" { _, err := a.db.Exec(deleteAllAccessQuery, username) @@ -325,3 +343,8 @@ func (a *SQLiteAuth) ResetAccess(username string, topic string) error { _, err := a.db.Exec(deleteTopicAccessQuery, username, topic) return err } + +// DefaultAccess returns the default read/write access if no access control entry matches +func (a *SQLiteAuth) DefaultAccess() (read bool, write bool) { + return a.defaultRead, a.defaultWrite +}