diff --git a/web/handlers/admin/denied_keys_test.go b/web/handlers/admin/denied_keys_test.go index 678f41d..cc38289 100644 --- a/web/handlers/admin/denied_keys_test.go +++ b/web/handlers/admin/denied_keys_test.go @@ -8,6 +8,7 @@ import ( "net/url" "testing" + "github.com/PuerkitoBio/goquery" "github.com/stretchr/testify/assert" "github.com/ssb-ngi-pointer/go-ssb-room/roomdb" @@ -32,6 +33,85 @@ func TestDeniedKeysEmpty(t *testing.T) { }) } +func TestDeniedKeysDisabledInterface(t *testing.T) { + ts := newSession(t) + a := assert.New(t) + + listURL := ts.URLTo(router.AdminDeniedKeysOverview) + + ts.User = roomdb.Member{ + ID: 1234, + Role: roomdb.RoleAdmin, + } + + html, resp := ts.Client.GetHTML(listURL) + a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code") + + formSelection := html.Find("form#add-entry") + a.EqualValues(1, formSelection.Length()) + + method, ok := formSelection.Attr("method") + a.True(ok, "form has method set") + a.Equal("POST", method) + + action, ok := formSelection.Attr("action") + a.True(ok, "form has action set") + + addURL := ts.URLTo(router.AdminDeniedKeysAdd) + a.Equal(addURL.String(), action) + + webassert.ElementsInForm(t, formSelection, []webassert.FormElement{ + {Name: "pub_key", Type: "text"}, + {Name: "comment", Type: "text"}, + }) + + newKey := "@x7iOLUcq3o+sjGeAnipvWeGzfuYgrXl8L4LYlxIhwDc=.ed25519" + addVals := url.Values{ + "comment": []string{"some comment"}, + // just any key that looks valid + "pub_key": []string{newKey}, + } + rec := ts.Client.PostForm(addURL, addVals) + a.Equal(http.StatusTemporaryRedirect, rec.Code) + + a.Equal(1, ts.DeniedKeysDB.AddCallCount()) + _, addedKey, addedComment := ts.DeniedKeysDB.AddArgsForCall(0) + a.Equal(newKey, addedKey.Ref()) + a.Equal("some comment", addedComment) + + /* Verify that the inputs are visible/hidden depending on user roles */ + checkInputsAreDisabled := func(shouldBeDisabled bool) { + html, resp = ts.Client.GetHTML(listURL) + a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code") + inputContainer := html.Find("#denied-keys-input-container") + a.Equal(1, inputContainer.Length()) + inputs := inputContainer.Find("input") + // pubkey, comment, submit button + a.Equal(3, inputs.Length()) + inputs.Each(func(i int, el *goquery.Selection) { + _, disabled := el.Attr("disabled") + a.Equal(shouldBeDisabled, disabled) + }) + } + + // verify that inputs are enabled for RoleAdmin + checkInputsAreDisabled(false) + + // verify that inputs are enabled for RoleModerator + ts.User = roomdb.Member{ + ID: 9001, + Role: roomdb.RoleModerator, + } + checkInputsAreDisabled(false) + + // verify that inputs are disabled for RoleMember + ts.User = roomdb.Member{ + ID: 7331, + Role: roomdb.RoleMember, + } + checkInputsAreDisabled(true) +} + func TestDeniedKeysAdd(t *testing.T) { ts := newSession(t) a := assert.New(t) diff --git a/web/handlers/admin/invites_test.go b/web/handlers/admin/invites_test.go index 2a32ceb..c074060 100644 --- a/web/handlers/admin/invites_test.go +++ b/web/handlers/admin/invites_test.go @@ -64,6 +64,50 @@ func TestInvitesOverview(t *testing.T) { a.True(yes, "a-tag has href attribute") wantURL := ts.URLTo(router.AdminInvitesRevokeConfirm, "id", 666) a.Equal(wantURL.String(), link) + + testInviteButtonDisabled := func(shouldBeDisabled bool) { + html, resp = ts.Client.GetHTML(invitesOverviewURL) + a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code") + inviteButton := html.Find("#create-invite button") + _, disabled := inviteButton.Attr("disabled") + a.EqualValues(shouldBeDisabled, disabled, "invite button should be disabled") + } + + // member, mod, admin should all be able to invite in ModeCommunity + ts.ConfigDB.GetPrivacyModeReturns(roomdb.ModeCommunity, nil) + ts.User = roomdb.Member{ + ID: 1234, + Role: roomdb.RoleAdmin, + } + testInviteButtonDisabled(false) + ts.User = roomdb.Member{ + ID: 7331, + Role: roomdb.RoleModerator, + } + testInviteButtonDisabled(false) + ts.User = roomdb.Member{ + ID: 9001, + Role: roomdb.RoleMember, + } + testInviteButtonDisabled(false) + + // mod and admin should be able to invite, member should not + ts.ConfigDB.GetPrivacyModeReturns(roomdb.ModeRestricted, nil) + ts.User = roomdb.Member{ + ID: 1234, + Role: roomdb.RoleAdmin, + } + testInviteButtonDisabled(false) + ts.User = roomdb.Member{ + ID: 7331, + Role: roomdb.RoleModerator, + } + testInviteButtonDisabled(false) + ts.User = roomdb.Member{ + ID: 9001, + Role: roomdb.RoleMember, + } + testInviteButtonDisabled(true) } func TestInvitesCreateForm(t *testing.T) { diff --git a/web/handlers/admin/members_test.go b/web/handlers/admin/members_test.go index 0d870f4..ac24bb0 100644 --- a/web/handlers/admin/members_test.go +++ b/web/handlers/admin/members_test.go @@ -7,6 +7,7 @@ import ( "net/url" "testing" + "github.com/PuerkitoBio/goquery" "github.com/stretchr/testify/assert" "github.com/ssb-ngi-pointer/go-ssb-room/roomdb" @@ -35,6 +36,11 @@ func TestMembersAdd(t *testing.T) { ts := newSession(t) a := assert.New(t) + ts.User = roomdb.Member{ + ID: 1234, + Role: roomdb.RoleAdmin, + } + listURL := ts.URLTo(router.AdminMembersOverview) html, resp := ts.Client.GetHTML(listURL) @@ -70,6 +76,43 @@ func TestMembersAdd(t *testing.T) { a.Equal(newKey, addedPubKey.Ref()) a.Equal(roomdb.RoleMember, addedRole) + /* Verify that the inputs are visible/hidden depending on user roles */ + checkInputsAreDisabled := func(shouldBeDisabled bool) { + html, resp = ts.Client.GetHTML(listURL) + a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code") + inputContainer := html.Find("#add-member-input-container") + a.Equal(1, inputContainer.Length()) + inputs := inputContainer.Find("input") + // pubkey + a.Equal(1, inputs.Length()) + inputs.Each(func(i int, el *goquery.Selection) { + _, disabled := el.Attr("disabled") + a.Equal(shouldBeDisabled, disabled) + }) + button := inputContainer.Find("button") + a.Equal(1, button.Length()) + button.Each(func(i int, el *goquery.Selection) { + _, disabled := el.Attr("disabled") + a.Equal(shouldBeDisabled, disabled) + }) + } + + // verify that inputs are enabled for RoleAdmin + checkInputsAreDisabled(false) + + // verify that inputs are enabled for RoleModerator + ts.User = roomdb.Member{ + ID: 9001, + Role: roomdb.RoleModerator, + } + checkInputsAreDisabled(false) + + // verify that inputs are disabled for RoleMember + ts.User = roomdb.Member{ + ID: 7331, + Role: roomdb.RoleMember, + } + checkInputsAreDisabled(true) } func TestMembersDontAddInvalid(t *testing.T) { @@ -173,6 +216,11 @@ func TestMemberDetails(t *testing.T) { memberURL := ts.URLTo(router.AdminMemberDetails, "id", "1") + ts.User = roomdb.Member{ + ID: 1234, + Role: roomdb.RoleAdmin, + } + html, resp := ts.Client.GetHTML(memberURL) a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code") @@ -180,14 +228,6 @@ func TestMemberDetails(t *testing.T) { {"title", "AdminMemberDetailsTitle"}, }) - // check for SSB ID - ssbID := html.Find("#ssb-id") - a.Equal(feedRef.Ref(), ssbID.Text()) - - // check for change-role dropdown - roleDropdown := html.Find("#change-role") - a.EqualValues(roleDropdown.Length(), 1) - aliasList := html.Find("#alias-list").Find("a") // check for link to resolve 1st Alias @@ -220,6 +260,37 @@ func TestMemberDetails(t *testing.T) { a.True(yes, "a-tag has href attribute") wantLink = ts.URLTo(router.AdminMembersRemoveConfirm, "id", 1) a.Equal(wantLink.String(), removeLink) + + testDisabledBehaviour := func(isElevated bool) { + html, resp := ts.Client.GetHTML(memberURL) + a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code") + // check for SSB ID + ssbID := html.Find("#ssb-id") + a.Equal(feedRef.Ref(), ssbID.Text()) + + // check for change-role dropdown + roleDropdown := html.Find("#change-role") + if isElevated { + a.Equal(1, roleDropdown.Length()) + } else { + a.Equal(0, roleDropdown.Length()) + } + } + testDisabledBehaviour(true) + + /* Now: verify that moderators cannot make room settings changes */ + ts.User = roomdb.Member{ + ID: 7331, + Role: roomdb.RoleModerator, + } + testDisabledBehaviour(true) + + /* Finally: verify that members cannot make room settings changes */ + ts.User = roomdb.Member{ + ID: 9001, + Role: roomdb.RoleMember, + } + testDisabledBehaviour(false) } func TestMembersRemoveConfirmation(t *testing.T) { diff --git a/web/handlers/admin/set_language_test.go b/web/handlers/admin/set_language_test.go index 9d57d11..af5b5a1 100644 --- a/web/handlers/admin/set_language_test.go +++ b/web/handlers/admin/set_language_test.go @@ -5,6 +5,7 @@ import ( "strings" "testing" + "github.com/ssb-ngi-pointer/go-ssb-room/roomdb" "github.com/ssb-ngi-pointer/go-ssb-room/web/router" "github.com/stretchr/testify/assert" ) @@ -33,6 +34,10 @@ func TestLanguageSetDefaultLanguage(t *testing.T) { a := assert.New(t) ts.ConfigDB.GetDefaultLanguageReturns("de", nil) + ts.User = roomdb.Member{ + ID: 1234, + Role: roomdb.RoleAdmin, + } u := ts.URLTo(router.AdminSettings) html, resp := ts.Client.GetHTML(u) diff --git a/web/handlers/admin/settings_test.go b/web/handlers/admin/settings_test.go new file mode 100644 index 0000000..c05c3a8 --- /dev/null +++ b/web/handlers/admin/settings_test.go @@ -0,0 +1,100 @@ +package admin + +import ( + "net/http" + "strings" + "testing" + + "github.com/PuerkitoBio/goquery" + "github.com/stretchr/testify/assert" + + "github.com/ssb-ngi-pointer/go-ssb-room/roomdb" + "github.com/ssb-ngi-pointer/go-ssb-room/web/router" +) + +func TestSettingsOverview(t *testing.T) { + ts := newSession(t) + a := assert.New(t) + + /* First: make sure everything renders correctly for admins */ + ts.User = roomdb.Member{ + ID: 1234, + Role: roomdb.RoleAdmin, + } + + ts.ConfigDB.GetPrivacyModeReturns(roomdb.ModeCommunity, nil) + ts.ConfigDB.GetDefaultLanguageReturns("en", nil) + + settingsURL := ts.URLTo(router.AdminSettings) + + html, resp := ts.Client.GetHTML(settingsURL) + a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code") + + // the privacy mode form & its summary/details container should exist + privacyFormContainer := html.Find("#change-privacy") + a.Equal(1, privacyFormContainer.Length()) + a.Equal(1, privacyFormContainer.Find("summary").Length()) + // chosen privacy mode is ModeCommunity (english translation will only be the name of the label, due to testing suite is set up atm) + a.Equal("ModeCommunity", strings.TrimSpace(privacyFormContainer.Find("summary").Text())) + // details-dropdown should have two forms, one for each of the other two privacy modes + // that can be selected (ModeOpen, ModeRestricted) + a.Equal(2, privacyFormContainer.Find("form").Length()) + // and one span, showing the selected mode + a.Equal(1, privacyFormContainer.Find("#selected-mode").Length()) + inputs := privacyFormContainer.Find("input") + // verify none of the privacy mode container's inputs are disabled + inputs.Each(func(i int, el *goquery.Selection) { + _, exists := el.Attr("disabled") + a.False(exists) + }) + + // verify that the change language form exists & is enabled + languageFormContainer := html.Find("#change-language-container") + a.Equal(1, languageFormContainer.Length()) + a.Equal(1, languageFormContainer.Find("summary").Length()) + // (english translation will only be the name of the label, due to testing suite is set up atm) + a.Equal("LanguageName", strings.TrimSpace(languageFormContainer.Find("summary").Text())) + + testDisabledBehaviour := func() { + settingsURL := ts.URLTo(router.AdminSettings) + html, resp := ts.Client.GetHTML(settingsURL) + a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code") + + // we do not have the summary/details hack if the forms are hidden + privacyFormContainer := html.Find("#change-privacy") + a.Equal(0, privacyFormContainer.Length()) + // the should still be the parent container, however + privacyContainer := html.Find("#privacy-mode-container") + a.Equal(1, privacyContainer.Length()) + // there should only be one input in the privacy mode container now + inputs := privacyContainer.Find("input") + a.Equal(1, inputs.Length()) + // the input should be disabled + _, disabled := inputs.Attr("disabled") + a.True(disabled) + + // next, verify that the change language setting is disabled + languageContainer := html.Find("#change-language-container") + a.Equal(1, languageContainer.Length()) + // there should only be one input in the language mode container now + inputs = languageContainer.Find("input") + a.Equal(1, inputs.Length()) + // the input should be disabled + _, disabled = inputs.Attr("disabled") + a.True(disabled) + } + + /* Now: verify that moderators cannot make room settings changes */ + ts.User = roomdb.Member{ + ID: 7331, + Role: roomdb.RoleModerator, + } + testDisabledBehaviour() + + /* Finally: verify that members cannot make room settings changes */ + ts.User = roomdb.Member{ + ID: 9001, + Role: roomdb.RoleMember, + } + testDisabledBehaviour() +} diff --git a/web/handlers/admin/setup_test.go b/web/handlers/admin/setup_test.go index c8bb2f8..07ce1e1 100644 --- a/web/handlers/admin/setup_test.go +++ b/web/handlers/admin/setup_test.go @@ -133,6 +133,15 @@ func newSession(t *testing.T) *testSession { testFuncs["urlToNotice"] = func(name string) string { return "" } testFuncs["language_count"] = func() int { return 1 } testFuncs["list_languages"] = func(*url.URL, string) string { return "" } + testFuncs["member_is_elevated"] = func() bool { return ts.User.Role == roomdb.RoleAdmin || ts.User.Role == roomdb.RoleModerator } + testFuncs["member_is_admin"] = func() bool { return ts.User.Role == roomdb.RoleAdmin } + testFuncs["member_can_invite"] = func() bool { + pm, _ := ts.ConfigDB.GetPrivacyMode(ctx) + memberElevated := ts.User.Role == roomdb.RoleAdmin || ts.User.Role == roomdb.RoleModerator + memberCanInvite := ts.User.Role == roomdb.RoleMember && (pm == roomdb.ModeCommunity || pm == roomdb.ModeOpen) + return memberElevated || memberCanInvite + } + testFuncs["list_languages"] = func(*url.URL, string) string { return "" } testFuncs["relative_time"] = func(when time.Time) string { return humanize.Time(when) } renderOpts := []render.Option{ diff --git a/web/handlers/http.go b/web/handlers/http.go index ed6fefd..5233860 100644 --- a/web/handlers/http.go +++ b/web/handlers/http.go @@ -123,6 +123,31 @@ func New( } }), + render.InjectTemplateFunc("member_can_invite", func(r *http.Request) interface{} { + return func() (bool, error) { + member := members.FromContext(r.Context()) + if member == nil { + return false, nil + } + + pm, err := dbs.Config.GetPrivacyMode(r.Context()) + if err != nil { + return false, err + } + + switch pm { + case roomdb.ModeOpen: + return true, nil + case roomdb.ModeCommunity: + return member.Role > roomdb.RoleUnknown && member.Role <= roomdb.RoleAdmin, nil + case roomdb.ModeRestricted: + return member.Role == roomdb.RoleAdmin || member.Role == roomdb.RoleModerator, nil + default: + return false, nil + } + } + }), + render.InjectTemplateFunc("language_count", func(r *http.Request) interface{} { return func() int { return len(locHelper.ListLanguages()) diff --git a/web/handlers/notices_test.go b/web/handlers/notices_test.go index 3208bf8..fc6ef27 100644 --- a/web/handlers/notices_test.go +++ b/web/handlers/notices_test.go @@ -109,7 +109,7 @@ func TestNoticesEditButtonVisible(t *testing.T) { } // have the database return okay for any user - testUser := roomdb.Member{ID: 23} + testUser := roomdb.Member{ID: 23, Role: roomdb.RoleAdmin} ts.AuthFallbackDB.CheckReturns(testUser.ID, nil) ts.MembersDB.GetByIDReturns(testUser, nil) diff --git a/web/i18n/defaults/active.de.toml b/web/i18n/defaults/active.de.toml index 6bfea8d..2490c94 100644 --- a/web/i18n/defaults/active.de.toml +++ b/web/i18n/defaults/active.de.toml @@ -117,6 +117,7 @@ AdminMemberDetailsRemove = "Mitglied entfernen" AdminMemberAdded = "Mitglied erfolgreich hinzugefügt." AdminMemberUpdated = "Mitglied aktualisiert." AdminMemberRemoved = "Mitglied entfernt." +AdminAddNewMemberTitle = "Add a new member" AdminAliasesRevoke = "Widerrufen" AdminAliasesRevokeConfirmTitle = "Alias widerrufen" diff --git a/web/i18n/defaults/active.en.toml b/web/i18n/defaults/active.en.toml index cd222a3..40c7f2b 100644 --- a/web/i18n/defaults/active.en.toml +++ b/web/i18n/defaults/active.en.toml @@ -124,6 +124,7 @@ AdminMemberDetailsRemove = "Remove member" AdminMemberAdded = "Member added successfully." AdminMemberUpdated = "Member updated." AdminMemberRemoved = "Member removed." +AdminAddNewMemberTitle = "Add a new member" AdminAliasesRevoke = "Revoke" AdminAliasesRevokeConfirmTitle = "Revoke Alias" diff --git a/web/members/helper.go b/web/members/helper.go index 1e2bf98..42542ed 100644 --- a/web/members/helper.go +++ b/web/members/helper.go @@ -92,13 +92,15 @@ func ContextInjecter(mdb roomdb.MembersService, withPassword *auth.Handler, with } // TemplateHelpers returns functions to be used with the go.mindeco.de/http/render package. -// Each has to return a function twice because the first is evaluated with the request before it gets passed onto html/template's FuncMap. +// Each helper has to return a function twice because the first is evaluated with the request before it gets passed onto html/template's FuncMap. // -// {{ is_logged_in }} returns true or false depending if the user is logged in +// {{ is_logged_in }} returns true or false depending on if the user is logged in // // {{ member_has_role "string" }} returns a boolean which confrms wether the member has a certain role (RoleMemeber, RoleAdmin, etc) // // {{ member_is_admin }} is a shortcut for {{ member_has_role "RoleAdmin" }} +// +// {{ member_is_elevated }} is a shortcut for {{ or member_has_role "RoleAdmin" member_has_role "RoleModerator"}} func TemplateHelpers() []render.Option { return []render.Option{ @@ -143,5 +145,19 @@ func TemplateHelpers() []render.Option { return member.Role == roomdb.RoleAdmin } }), + + // shorthand for is admin || mod (used for editing notices, managing users, managing aliases) + render.InjectTemplateFunc("member_is_elevated", func(r *http.Request) interface{} { + no := func() bool { return false } + + member := FromContext(r.Context()) + if member == nil { + return no + } + + return func() bool { + return member.Role == roomdb.RoleAdmin || member.Role == roomdb.RoleModerator + } + }), } } diff --git a/web/templates/admin/denied-keys.tmpl b/web/templates/admin/denied-keys.tmpl index 803b757..c082bfc 100644 --- a/web/templates/admin/denied-keys.tmpl +++ b/web/templates/admin/denied-keys.tmpl @@ -20,23 +20,34 @@ method="POST" > {{ .csrfField }} -
{{.Member.PubKey.Ref}}
-{{i18n .String}}
+ {{end}} {{end}} -{{ i18n "ExplanationPrivacyModes" }} {{ i18n "RoomsSpecification" }}.
{{ i18n "ExplanationDefaultLanguage" }}