Răsfoiți Sursa

Add wildcard access control

Philipp Heckel 4 ani în urmă
părinte
comite
c56814e7da
4 a modificat fișierele cu 56 adăugiri și 24 ștergeri
  1. 23 5
      auth/auth.go
  2. 28 18
      auth/auth_sqlite.go
  3. 3 0
      auth/auth_sqlite_test.go
  4. 2 1
      server/server.go

+ 23 - 5
auth/auth.go

@@ -1,6 +1,9 @@
 package auth
 
-import "errors"
+import (
+	"errors"
+	"regexp"
+)
 
 // Auther is a generic interface to implement password-based authentication and authorization
 type Auther interface {
@@ -39,12 +42,12 @@ type Manager interface {
 	ChangeRole(username string, role Role) error
 
 	// 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
+	// read/write access to a topic. The parameter topicPattern may include wildcards (*).
+	AllowAccess(username string, topicPattern 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
+	// empty) for an entire user. The parameter topicPattern may include wildcards (*).
+	ResetAccess(username string, topicPattern string) error
 
 	// DefaultAccess returns the default read/write access if no access control entry matches
 	DefaultAccess() (read bool, write bool)
@@ -89,11 +92,26 @@ const (
 	Everyone = "*"
 )
 
+var (
+	allowedUsernameRegex     = regexp.MustCompile(`^[-_.@a-zA-Z0-9]+$`)     // Does not include Everyone (*)
+	allowedTopicPatternRegex = regexp.MustCompile(`^[-_*A-Za-z0-9]{1,64}$`) // Adds '*' for wildcards!
+)
+
 // AllowedRole returns true if the given role can be used for new users
 func AllowedRole(role Role) bool {
 	return role == RoleUser || role == RoleAdmin
 }
 
+// AllowedUsername returns true if the given username is valid
+func AllowedUsername(username string) bool {
+	return allowedUsernameRegex.MatchString(username)
+}
+
+// AllowedTopicPattern returns true if the given topic pattern is valid; this includes the wildcard character (*)
+func AllowedTopicPattern(username string) bool {
+	return allowedTopicPatternRegex.MatchString(username)
+}
+
 // Error constants used by the package
 var (
 	ErrUnauthenticated = errors.New("unauthenticated")

+ 28 - 18
auth/auth_sqlite.go

@@ -4,7 +4,7 @@ import (
 	"database/sql"
 	_ "github.com/mattn/go-sqlite3" // SQLite driver
 	"golang.org/x/crypto/bcrypt"
-	"regexp"
+	"strings"
 )
 
 const (
@@ -37,7 +37,7 @@ const (
 	selectTopicPermsQuery = `
 		SELECT read, write 
 		FROM access 
-		WHERE user IN ('*', ?) AND topic = ?
+		WHERE user IN ('*', ?) AND ? LIKE topic
 		ORDER BY user DESC
 	`
 )
@@ -69,10 +69,6 @@ type SQLiteAuth struct {
 	defaultWrite bool
 }
 
-var (
-	allowedUsernameRegex = regexp.MustCompile(`^[-_.@a-zA-Z0-9]+$`)
-)
-
 var _ Auther = (*SQLiteAuth)(nil)
 var _ Manager = (*SQLiteAuth)(nil)
 
@@ -161,7 +157,7 @@ func (a *SQLiteAuth) resolvePerms(read, write bool, perm Permission) error {
 // 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) || !AllowedRole(role) {
+	if !AllowedUsername(username) || !AllowedRole(role) {
 		return ErrInvalidArgument
 	}
 	hash, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost)
@@ -177,7 +173,7 @@ func (a *SQLiteAuth) 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.
 func (a *SQLiteAuth) RemoveUser(username string) error {
-	if !allowedUsernameRegex.MatchString(username) || username == Everyone {
+	if !AllowedUsername(username) {
 		return ErrInvalidArgument
 	}
 	if _, err := a.db.Exec(deleteUserQuery, username); err != nil {
@@ -284,7 +280,7 @@ func (a *SQLiteAuth) readGrants(username string) ([]Grant, error) {
 			return nil, err
 		}
 		grants = append(grants, Grant{
-			Topic: topic,
+			Topic: fromSQLWildcard(topic),
 			Read:  read,
 			Write: write,
 		})
@@ -307,7 +303,7 @@ func (a *SQLiteAuth) 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.
 func (a *SQLiteAuth) ChangeRole(username string, role Role) error {
-	if !allowedUsernameRegex.MatchString(username) || !AllowedRole(role) {
+	if !AllowedUsername(username) || !AllowedRole(role) {
 		return ErrInvalidArgument
 	}
 	if _, err := a.db.Exec(updateUserRoleQuery, string(role), username); err != nil {
@@ -322,25 +318,31 @@ func (a *SQLiteAuth) ChangeRole(username string, role Role) error {
 }
 
 // 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 {
+// read/write access to a topic. The parameter topicPattern may include wildcards (*).
+func (a *SQLiteAuth) AllowAccess(username string, topicPattern string, read bool, write bool) error {
+	if (!AllowedUsername(username) && username != Everyone) || !AllowedTopicPattern(topicPattern) {
+		return ErrInvalidArgument
+	}
+	if _, err := a.db.Exec(upsertUserAccessQuery, username, toSQLWildcard(topicPattern), read, write); err != nil {
 		return err
 	}
 	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 == "" {
+// empty) for an entire user. The parameter topicPattern may include wildcards (*).
+func (a *SQLiteAuth) ResetAccess(username string, topicPattern string) error {
+	if (!AllowedUsername(username) && username != Everyone) || (!AllowedTopicPattern(topicPattern) && topicPattern != "") {
+		return ErrInvalidArgument
+	}
+	if username == "" && topicPattern == "" {
 		_, err := a.db.Exec(deleteAllAccessQuery, username)
 		return err
-	} else if topic == "" {
+	} else if topicPattern == "" {
 		_, err := a.db.Exec(deleteUserAccessQuery, username)
 		return err
 	}
-	_, err := a.db.Exec(deleteTopicAccessQuery, username, topic)
+	_, err := a.db.Exec(deleteTopicAccessQuery, username, toSQLWildcard(topicPattern))
 	return err
 }
 
@@ -348,3 +350,11 @@ func (a *SQLiteAuth) ResetAccess(username string, topic string) error {
 func (a *SQLiteAuth) DefaultAccess() (read bool, write bool) {
 	return a.defaultRead, a.defaultWrite
 }
+
+func toSQLWildcard(s string) string {
+	return strings.ReplaceAll(s, "*", "%")
+}
+
+func fromSQLWildcard(s string) string {
+	return strings.ReplaceAll(s, "%", "*")
+}

+ 3 - 0
auth/auth_sqlite_test.go

@@ -19,6 +19,7 @@ func TestSQLiteAuth_FullScenario_Default_DenyAll(t *testing.T) {
 	require.Nil(t, a.AllowAccess("ben", "everyonewrite", false, false)) // How unfair!
 	require.Nil(t, a.AllowAccess(auth.Everyone, "announcements", true, false))
 	require.Nil(t, a.AllowAccess(auth.Everyone, "everyonewrite", true, true))
+	require.Nil(t, a.AllowAccess(auth.Everyone, "up*", false, true)) // Everyone can write to /up*
 
 	phil, err := a.Authenticate("phil", "phil")
 	require.Nil(t, err)
@@ -77,6 +78,8 @@ func TestSQLiteAuth_FullScenario_Default_DenyAll(t *testing.T) {
 	require.Nil(t, a.Authorize(nil, "announcements", auth.PermissionRead))
 	require.Nil(t, a.Authorize(nil, "everyonewrite", auth.PermissionRead))
 	require.Nil(t, a.Authorize(nil, "everyonewrite", auth.PermissionWrite))
+	require.Nil(t, a.Authorize(nil, "up1234", auth.PermissionWrite)) // Wildcard permission
+	require.Nil(t, a.Authorize(nil, "up5678", auth.PermissionWrite))
 }
 
 func TestSQLiteAuth_AddUser_Invalid(t *testing.T) {

+ 2 - 1
server/server.go

@@ -63,6 +63,7 @@ type indexPage struct {
 type handleFunc func(http.ResponseWriter, *http.Request, *visitor) error
 
 var (
+	// If changed, don't forget to update Android App and auth_sqlite.go
 	topicRegex       = regexp.MustCompile(`^[-_A-Za-z0-9]{1,64}$`)  // No /!
 	topicPathRegex   = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}$`) // Regex must match JS & Android app!
 	jsonPathRegex    = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/json$`)
@@ -75,7 +76,7 @@ var (
 	staticRegex      = regexp.MustCompile(`^/static/.+`)
 	docsRegex        = regexp.MustCompile(`^/docs(|/.*)$`)
 	fileRegex        = regexp.MustCompile(`^/file/([-_A-Za-z0-9]{1,64})(?:\.[A-Za-z0-9]{1,16})?$`)
-	disallowedTopics = []string{"docs", "static", "file"}
+	disallowedTopics = []string{"docs", "static", "file"} // If updated, also update in Android app
 	attachURLRegex   = regexp.MustCompile(`^https?://`)
 
 	templateFnMap = template.FuncMap{