-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Persistent tokens and hook deletion #22
Changes from 14 commits
d47222a
b52d782
4665f31
d60cf3f
b4fcf52
d0006f0
1c40a4b
680e0b0
d1ac53c
eabed3b
d4698be
c0b76cb
11e4546
cd78d13
2b274a0
2b0c48e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,7 @@ sudo: required | |
matrix: | ||
include: | ||
- go: "1.10.x" | ||
- go: "1.11.x" | ||
- go: tip | ||
allow_failures: | ||
- go: tip | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,9 +8,11 @@ import ( | |
"net/http" | ||
"net/url" | ||
"path" | ||
"strconv" | ||
"strings" | ||
|
||
"github.com/G-Node/gin-cli/ginclient" | ||
gweb "github.com/G-Node/gin-cli/web" | ||
"github.com/G-Node/gin-valid/config" | ||
"github.com/G-Node/gin-valid/helpers" | ||
"github.com/G-Node/gin-valid/log" | ||
|
@@ -26,35 +28,56 @@ func EnableHook(w http.ResponseWriter, r *http.Request) { | |
user := vars["user"] | ||
repo := vars["repo"] | ||
validator := strings.ToLower(vars["validator"]) | ||
cfg := config.Read() | ||
cookiename := cfg.Settings.CookieName | ||
sessionid, err := r.Cookie(cookiename) | ||
ut, err := getSessionOrRedirect(w, r) | ||
if err != nil { | ||
return | ||
} | ||
if !helpers.SupportedValidator(validator) { | ||
fail(w, http.StatusNotFound, "unsupported validator") | ||
return | ||
} | ||
repopath := fmt.Sprintf("%s/%s", user, repo) | ||
err = createValidHook(repopath, validator, ut) | ||
if err != nil { | ||
msg := fmt.Sprintf("Hook creation failed: unauthorised") | ||
fail(w, http.StatusUnauthorized, msg) | ||
// TODO: Check if failure is for other reasons and maybe return 500 instead | ||
fail(w, http.StatusUnauthorized, err.Error()) | ||
return | ||
} | ||
http.Redirect(w, r, fmt.Sprintf("/repos/%s", ut.Username), http.StatusFound) | ||
} | ||
|
||
session, ok := sessions[sessionid.Value] | ||
if !ok { | ||
msg := fmt.Sprintf("Hook creation failed: unauthorised") | ||
fail(w, http.StatusUnauthorized, msg) | ||
func DisableHook(w http.ResponseWriter, r *http.Request) { | ||
if r.Method != "GET" { | ||
return | ||
} | ||
if !helpers.SupportedValidator(validator) { | ||
fail(w, http.StatusNotFound, "unsupported validator") | ||
vars := mux.Vars(r) | ||
user := vars["user"] | ||
repo := vars["repo"] | ||
hookidstr := vars["hookid"] | ||
|
||
hookid, err := strconv.Atoi(hookidstr) | ||
if err != nil { | ||
// bad hook ID (not a number): throw a generic 404 | ||
fail(w, http.StatusNotFound, "not found") | ||
return | ||
} | ||
|
||
ut, err := getSessionOrRedirect(w, r) | ||
if err != nil { | ||
return | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See |
||
} | ||
|
||
repopath := fmt.Sprintf("%s/%s", user, repo) | ||
err = createValidHook(repopath, validator, session) | ||
err = deleteValidHook(repopath, hookid, ut) | ||
if err != nil { | ||
// TODO: Check if failure is for other reasons and maybe return 500 instead | ||
fail(w, http.StatusUnauthorized, err.Error()) | ||
return | ||
} | ||
http.Redirect(w, r, fmt.Sprintf("/repos/%s", session.Username), http.StatusFound) | ||
http.Redirect(w, r, fmt.Sprintf("/repos/%s", ut.Username), http.StatusFound) | ||
} | ||
|
||
func validateHookSecret(data []byte, secret string) bool { | ||
func checkHookSecret(data []byte, secret string) bool { | ||
cfg := config.Read() | ||
hooksecret := cfg.Settings.HookSecret | ||
sig := hmac.New(sha256.New, []byte(hooksecret)) | ||
|
@@ -63,20 +86,19 @@ func validateHookSecret(data []byte, secret string) bool { | |
return signature == secret | ||
} | ||
|
||
func createValidHook(repopath string, validator string, session *usersession) error { | ||
func createValidHook(repopath string, validator string, usertoken gweb.UserToken) error { | ||
// TODO: AVOID DUPLICATES: | ||
// - If it's already hooked and we have it on record, do nothing | ||
// - If it's already hooked, but we don't know about it, check if it's valid and don't recreate | ||
log.Write("Adding %s hook to %s\n", validator, repopath) | ||
|
||
gvconfig := config.Read() | ||
cfg := config.Read() | ||
client := ginclient.New(serveralias) | ||
client.UserToken = session.UserToken | ||
client.UserToken = usertoken | ||
hookconfig := make(map[string]string) | ||
cfg := config.Read() | ||
hooksecret := cfg.Settings.HookSecret | ||
|
||
host := fmt.Sprintf("%s:%s", gvconfig.Settings.RootURL, gvconfig.Settings.Port) | ||
host := fmt.Sprintf("%s:%s", cfg.Settings.RootURL, cfg.Settings.Port) | ||
u, err := url.Parse(host) | ||
u.Path = path.Join(u.Path, "validate", validator, repopath) | ||
hookconfig["url"] = u.String() | ||
|
@@ -90,35 +112,40 @@ func createValidHook(repopath string, validator string, session *usersession) er | |
} | ||
res, err := client.Post(fmt.Sprintf("/api/v1/repos/%s/hooks", repopath), data) | ||
if err != nil { | ||
log.Write("[error] failed to post: %s\n", err.Error()) | ||
log.Write("[error] failed to post: %s", err.Error()) | ||
return fmt.Errorf("Hook creation failed: %s", err.Error()) | ||
} | ||
defer res.Body.Close() | ||
|
||
if res.StatusCode != http.StatusCreated { | ||
log.Write("[error] non-OK response: %s\n", res.Status) | ||
log.Write("[error] non-OK response: %s", res.Status) | ||
return fmt.Errorf("Hook creation failed: %s", res.Status) | ||
} | ||
hookregs[repopath] = session.UserToken | ||
return nil | ||
|
||
// link user token to repository name so we can use it for validation | ||
return linkToRepo(usertoken.Username, repopath) | ||
} | ||
|
||
func deleteValidHook(repopath string, id int) { | ||
func deleteValidHook(repopath string, id int, usertoken gweb.UserToken) error { | ||
log.Write("Deleting %d from %s\n", id, repopath) | ||
|
||
client := ginclient.New(serveralias) | ||
err := client.LoadToken() | ||
if err != nil { | ||
log.Write("[error] failed to load token %s\n", err.Error()) | ||
return | ||
} | ||
client.UserToken = usertoken | ||
|
||
res, err := client.Delete(fmt.Sprintf("/api/v1/repos/%s/hooks/%d", repopath, id)) | ||
if err != nil { | ||
log.Write("[error] bad response from server %s\n", err.Error()) | ||
return | ||
log.Write("[error] bad response from server %s", err.Error()) | ||
return err | ||
} | ||
defer res.Body.Close() | ||
// fmt.Printf("Got response: %s\n", res.Status) | ||
// bdy, _ := ioutil.ReadAll(res.Body) | ||
// fmt.Println(string(bdy)) | ||
log.Write("[info] removed hook for %s", repopath) | ||
|
||
log.Write("[info] removing repository -> token link") | ||
err = rmTokenRepoLink(repopath) | ||
if err != nil { | ||
log.Write("[error] failed to delete token link: %s", err.Error()) | ||
// don't fail | ||
} | ||
|
||
return nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
package web | ||
|
||
import ( | ||
"encoding/base32" | ||
"encoding/gob" | ||
"os" | ||
"path/filepath" | ||
|
||
gweb "github.com/G-Node/gin-cli/web" | ||
"github.com/G-Node/gin-valid/config" | ||
"github.com/G-Node/gin-valid/log" | ||
) | ||
|
||
// saveToken writes a token to disk using the username as filename. | ||
// The location is defined by config.Dir.Tokens. | ||
func saveToken(ut gweb.UserToken) error { | ||
cfg := config.Read() | ||
filename := filepath.Join(cfg.Dir.Tokens, ut.Username) | ||
tokenfile, err := os.Create(filename) | ||
defer tokenfile.Close() | ||
if err != nil { | ||
return err | ||
} | ||
encoder := gob.NewEncoder(tokenfile) | ||
return encoder.Encode(ut) | ||
} | ||
|
||
// loadUserToken reads a token from disk using the username as filename. | ||
// The location is defined by config.Dir.Tokens. | ||
func loadUserToken(username string) (gweb.UserToken, error) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does it makes sense to use the name |
||
cfg := config.Read() | ||
filename := filepath.Join(cfg.Dir.Tokens, username) | ||
return loadToken(filename) | ||
} | ||
|
||
// loadToken loads a token from the provided path | ||
func loadToken(path string) (gweb.UserToken, error) { | ||
ut := gweb.UserToken{} | ||
tokenfile, err := os.Open(path) | ||
if err != nil { | ||
log.Write("[Error] Failed to load token from %s", path) | ||
return ut, err | ||
} | ||
defer tokenfile.Close() | ||
|
||
decoder := gob.NewDecoder(tokenfile) | ||
err = decoder.Decode(&ut) | ||
return ut, err | ||
} | ||
|
||
// linkToSession links a sessionID to a user's token. | ||
func linkToSession(username string, sessionid string) error { | ||
cfg := config.Read() | ||
tokendir := cfg.Dir.Tokens | ||
utfile := filepath.Join(tokendir, username) | ||
sidfile := filepath.Join(tokendir, "by-sessionid", b32(sessionid)) | ||
return os.Symlink(utfile, sidfile) | ||
} | ||
|
||
// getTokenBySession loads a user's access token using the session ID found in | ||
// the user's cookie store. | ||
func getTokenBySession(sessionid string) (gweb.UserToken, error) { | ||
cfg := config.Read() | ||
tokendir := cfg.Dir.Tokens | ||
filename := filepath.Join(tokendir, "by-sessionid", b32(sessionid)) | ||
return loadToken(filename) | ||
} | ||
|
||
// linkToRepo links a repository name to a user's token. | ||
// This token will be used for cloning a repository to run a validator when a | ||
// web hook is triggered. | ||
func linkToRepo(username string, repopath string) error { | ||
cfg := config.Read() | ||
tokendir := cfg.Dir.Tokens | ||
utfile := filepath.Join(tokendir, username) | ||
sidfile := filepath.Join(tokendir, "by-repo", b32(repopath)) | ||
return os.Symlink(utfile, sidfile) | ||
} | ||
|
||
// getTokenByRepo loads a user's access token using a repository path. | ||
func getTokenByRepo(repopath string) (gweb.UserToken, error) { | ||
cfg := config.Read() | ||
tokendir := cfg.Dir.Tokens | ||
filename := filepath.Join(tokendir, "by-repo", b32(repopath)) | ||
return loadToken(filename) | ||
} | ||
|
||
// rmTokenRepoLink deletes a repository -> token link, removing our ability to | ||
// clone the repository. | ||
func rmTokenRepoLink(repopath string) error { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are tokens and session symlinks ever cleaned up? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nope. Opened issue #23. |
||
cfg := config.Read() | ||
tokendir := cfg.Dir.Tokens | ||
filename := filepath.Join(tokendir, "by-repo", b32(repopath)) | ||
return os.Remove(filename) | ||
} | ||
|
||
// b32 encodes a string to base 32. Use this to make strings such as IDs or | ||
// repopaths filename friendly. | ||
func b32(s string) string { | ||
return base32.StdEncoding.EncodeToString([]byte(s)) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error message is potentially not logged.
getSessionOrRedirect
logs in one error case and does not in another.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getSessionOrRedirect
logs the error only if the user has a session cookie set and the server doesn't have a session with that ID. This is the second case where the error isInvalid session found in cookie
. The second case,No session cookie found
isn't really an error, it just occurs when a user goes to a page that requires login and has no session cookie. I don't think it's necessary to log this, but I can add it if you want.