User-owned ACL entries
This commit is contained in:
parent
598d0bdda3
commit
2267d27c9b
9 changed files with 160 additions and 57 deletions
|
@ -41,7 +41,7 @@ var (
|
|||
errHTTPBadRequestDelayTooLarge = &errHTTP{40006, http.StatusBadRequest, "invalid delay parameter: too large, please refer to the docs", "https://ntfy.sh/docs/publish/#scheduled-delivery"}
|
||||
errHTTPBadRequestPriorityInvalid = &errHTTP{40007, http.StatusBadRequest, "invalid priority parameter", "https://ntfy.sh/docs/publish/#message-priority"}
|
||||
errHTTPBadRequestSinceInvalid = &errHTTP{40008, http.StatusBadRequest, "invalid since parameter", "https://ntfy.sh/docs/subscribe/api/#fetch-cached-messages"}
|
||||
errHTTPBadRequestTopicInvalid = &errHTTP{40009, http.StatusBadRequest, "invalid topic: path invalid", ""}
|
||||
errHTTPBadRequestTopicInvalid = &errHTTP{40009, http.StatusBadRequest, "invalid topic: topic invalid", ""}
|
||||
errHTTPBadRequestTopicDisallowed = &errHTTP{40010, http.StatusBadRequest, "invalid topic: topic name is disallowed", ""}
|
||||
errHTTPBadRequestMessageNotUTF8 = &errHTTP{40011, http.StatusBadRequest, "invalid message: message must be UTF-8 encoded", ""}
|
||||
errHTTPBadRequestAttachmentURLInvalid = &errHTTP{40013, http.StatusBadRequest, "invalid request: attachment URL is invalid", "https://ntfy.sh/docs/publish/#attachments"}
|
||||
|
@ -60,6 +60,7 @@ var (
|
|||
errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication"}
|
||||
errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication"}
|
||||
errHTTPConflictUserExists = &errHTTP{40901, http.StatusConflict, "conflict: user already exists", ""}
|
||||
errHTTPConflictTopicReserved = &errHTTP{40902, http.StatusConflict, "conflict: access control entry for topic or topic pattern already exists", ""}
|
||||
errHTTPEntityTooLargeAttachment = &errHTTP{41301, http.StatusRequestEntityTooLarge, "attachment too large, or bandwidth limit reached", "https://ntfy.sh/docs/publish/#limitations"}
|
||||
errHTTPEntityTooLargeMatrixRequest = &errHTTP{41302, http.StatusRequestEntityTooLarge, "Matrix request is larger than the max allowed length", ""}
|
||||
errHTTPEntityTooLargeJSONBody = &errHTTP{41303, http.StatusRequestEntityTooLarge, "JSON body too large", ""}
|
||||
|
@ -70,6 +71,6 @@ var (
|
|||
errHTTPTooManyRequestsAttachmentBandwidthLimit = &errHTTP{42905, http.StatusTooManyRequests, "too many requests: daily bandwidth limit reached", "https://ntfy.sh/docs/publish/#limitations"}
|
||||
errHTTPTooManyRequestsAccountCreateLimit = &errHTTP{42906, http.StatusTooManyRequests, "too many requests: daily account creation limit reached", "https://ntfy.sh/docs/publish/#limitations"} // FIXME document limit
|
||||
errHTTPInternalError = &errHTTP{50001, http.StatusInternalServerError, "internal server error", ""}
|
||||
errHTTPInternalErrorInvalidPath = &errHTTP{50002, http.StatusInternalServerError, "internal server error: invalid file path", ""}
|
||||
errHTTPInternalErrorInvalidPath = &errHTTP{50002, http.StatusInternalServerError, "internal server error: invalid path", ""}
|
||||
errHTTPInternalErrorMissingBaseURL = &errHTTP{50003, http.StatusInternalServerError, "internal server error: base-url must be be configured for this feature", "https://ntfy.sh/docs/config/"}
|
||||
)
|
||||
|
|
|
@ -103,6 +103,7 @@ var (
|
|||
accountSettingsPath = "/v1/account/settings"
|
||||
accountSubscriptionPath = "/v1/account/subscription"
|
||||
accountAccessPath = "/v1/account/access"
|
||||
accountAccessSingleRegex = regexp.MustCompile(`/v1/account/access/([-_A-Za-z0-9]{1,64})$`)
|
||||
accountSubscriptionSingleRegex = regexp.MustCompile(`^/v1/account/subscription/([-_A-Za-z0-9]{16})$`)
|
||||
matrixPushPath = "/_matrix/push/v1/notify"
|
||||
staticRegex = regexp.MustCompile(`^/static/.+`)
|
||||
|
@ -361,6 +362,8 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
|
|||
return s.ensureUser(s.handleAccountSubscriptionDelete)(w, r, v)
|
||||
} else if r.Method == http.MethodPost && r.URL.Path == accountAccessPath {
|
||||
return s.ensureUser(s.handleAccountAccessAdd)(w, r, v)
|
||||
} else if r.Method == http.MethodDelete && accountAccessSingleRegex.MatchString(r.URL.Path) {
|
||||
return s.ensureUser(s.handleAccountAccessDelete)(w, r, v)
|
||||
} else if r.Method == http.MethodGet && r.URL.Path == matrixPushPath {
|
||||
return s.handleMatrixDiscovery(w)
|
||||
} else if r.Method == http.MethodGet && staticRegex.MatchString(r.URL.Path) {
|
||||
|
|
|
@ -91,7 +91,18 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, r *http.Request, v *vis
|
|||
Upgradable: true,
|
||||
}
|
||||
}
|
||||
|
||||
if len(v.user.Grants) > 0 {
|
||||
response.Access = make([]*apiAccountGrant, 0)
|
||||
for _, grant := range v.user.Grants {
|
||||
if grant.Owner {
|
||||
response.Access = append(response.Access, &apiAccountGrant{
|
||||
Topic: grant.TopicPattern,
|
||||
Read: grant.AllowRead,
|
||||
Write: grant.AllowWrite,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
response.Username = user.Everyone
|
||||
response.Role = string(user.RoleAnonymous)
|
||||
|
@ -316,13 +327,46 @@ func (s *Server) handleAccountAccessAdd(w http.ResponseWriter, r *http.Request,
|
|||
if !topicRegex.MatchString(req.Topic) {
|
||||
return errHTTPBadRequestTopicInvalid
|
||||
}
|
||||
// FIXME authorize: how do I know if v.user (= auth'd user) is allowed to write the ACL entries
|
||||
if err := s.userManager.CheckAllowAccess(v.user.Name, req.Topic); err != nil {
|
||||
return errHTTPConflictTopicReserved
|
||||
}
|
||||
owner, username := v.user.Name, v.user.Name
|
||||
everyoneRead := util.Contains([]string{"read-write", "rw", "read-only", "read", "ro"}, req.Everyone)
|
||||
everyoneWrite := util.Contains([]string{"read-write", "rw", "write-only", "write", "wo"}, req.Everyone)
|
||||
if err := s.userManager.AllowAccess(v.user.Name, req.Topic, true, true); err != nil {
|
||||
if err := s.userManager.AllowAccess(owner, username, req.Topic, true, true); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.userManager.AllowAccess(user.Everyone, req.Topic, everyoneRead, everyoneWrite); err != nil {
|
||||
if err := s.userManager.AllowAccess(owner, user.Everyone, req.Topic, everyoneRead, everyoneWrite); err != nil {
|
||||
return err
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) handleAccountAccessDelete(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||
matches := accountAccessSingleRegex.FindStringSubmatch(r.URL.Path)
|
||||
if len(matches) != 2 {
|
||||
return errHTTPInternalErrorInvalidPath
|
||||
}
|
||||
topic := matches[1]
|
||||
if !topicRegex.MatchString(topic) {
|
||||
return errHTTPBadRequestTopicInvalid
|
||||
}
|
||||
authorized := false
|
||||
for _, grant := range v.user.Grants {
|
||||
if grant.TopicPattern == topic && grant.Owner {
|
||||
authorized = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !authorized {
|
||||
return errHTTPUnauthorized
|
||||
}
|
||||
if err := s.userManager.ResetAccess(v.user.Name, topic); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.userManager.ResetAccess(user.Everyone, topic); err != nil {
|
||||
return err
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
|
|
@ -643,7 +643,7 @@ func TestServer_Auth_Success_User(t *testing.T) {
|
|||
s := newTestServer(t, c)
|
||||
|
||||
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AllowAccess("ben", "mytopic", true, true))
|
||||
require.Nil(t, s.userManager.AllowAccess("", "ben", "mytopic", true, true))
|
||||
|
||||
response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{
|
||||
"Authorization": basicAuth("ben:ben"),
|
||||
|
@ -659,8 +659,8 @@ func TestServer_Auth_Success_User_MultipleTopics(t *testing.T) {
|
|||
s := newTestServer(t, c)
|
||||
|
||||
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AllowAccess("ben", "mytopic", true, true))
|
||||
require.Nil(t, s.userManager.AllowAccess("ben", "anothertopic", true, true))
|
||||
require.Nil(t, s.userManager.AllowAccess("", "ben", "mytopic", true, true))
|
||||
require.Nil(t, s.userManager.AllowAccess("", "ben", "anothertopic", true, true))
|
||||
|
||||
response := request(t, s, "GET", "/mytopic,anothertopic/auth", "", map[string]string{
|
||||
"Authorization": basicAuth("ben:ben"),
|
||||
|
@ -696,7 +696,7 @@ func TestServer_Auth_Fail_Unauthorized(t *testing.T) {
|
|||
s := newTestServer(t, c)
|
||||
|
||||
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AllowAccess("ben", "sometopic", true, true)) // Not mytopic!
|
||||
require.Nil(t, s.userManager.AllowAccess("", "ben", "sometopic", true, true)) // Not mytopic!
|
||||
|
||||
response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{
|
||||
"Authorization": basicAuth("ben:ben"),
|
||||
|
@ -712,8 +712,8 @@ func TestServer_Auth_Fail_CannotPublish(t *testing.T) {
|
|||
s := newTestServer(t, c)
|
||||
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin))
|
||||
require.Nil(t, s.userManager.AllowAccess(user.Everyone, "private", false, false))
|
||||
require.Nil(t, s.userManager.AllowAccess(user.Everyone, "announcements", true, false))
|
||||
require.Nil(t, s.userManager.AllowAccess("", user.Everyone, "private", false, false))
|
||||
require.Nil(t, s.userManager.AllowAccess("", user.Everyone, "announcements", true, false))
|
||||
|
||||
response := request(t, s, "PUT", "/mytopic", "test", nil)
|
||||
require.Equal(t, 200, response.Code)
|
||||
|
|
|
@ -256,12 +256,19 @@ type apiAccountStats struct {
|
|||
AttachmentTotalSizeRemaining int64 `json:"attachment_total_size_remaining"`
|
||||
}
|
||||
|
||||
type apiAccountGrant struct {
|
||||
Topic string `json:"topic"`
|
||||
Read bool `json:"read"`
|
||||
Write bool `json:"write"`
|
||||
}
|
||||
|
||||
type apiAccountResponse struct {
|
||||
Username string `json:"username"`
|
||||
Role string `json:"role,omitempty"`
|
||||
Language string `json:"language,omitempty"`
|
||||
Notification *user.NotificationPrefs `json:"notification,omitempty"`
|
||||
Subscriptions []*user.Subscription `json:"subscriptions,omitempty"`
|
||||
Access []*apiAccountGrant `json:"access,omitempty"`
|
||||
Plan *apiAccountPlan `json:"plan,omitempty"`
|
||||
Limits *apiAccountLimits `json:"limits,omitempty"`
|
||||
Stats *apiAccountStats `json:"stats,omitempty"`
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue