Skip to content
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

Testing / enhancing oauth integration #336

Merged
merged 8 commits into from
Mar 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apiserver/.dockerignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
*/build/*
**/build/*
**/vendor/*

.dockerignore
Expand Down
7 changes: 5 additions & 2 deletions apiserver/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,17 @@ COPY glide.* ./
RUN glide install --strip-vendor

# Build golang code
COPY . ./
COPY pkg ./pkg/
COPY cmd ./cmd/
COPY build.sh ./
RUN ./build.sh docker

# Create runtime container
FROM debian:buster

# Install runtime dependencies
RUN apt-get -qq update && \
apt-get -qq install --no-install-recommends binutils git ca-certificates netcat && \
apt-get -qq install --no-install-recommends binutils git ca-certificates netcat curl && \
apt-get -qq autoremove && \
apt-get -qq autoclean && \
apt-get -qq clean all && \
Expand All @@ -37,6 +39,7 @@ COPY --from=gobuild /go/src/github.com/ndslabs/apiserver/build/bin/ndslabsctl-*-
COPY --from=gobuild /go/src/github.com/ndslabs/apiserver/build/bin/apiserver-linux-amd64 /usr/local/bin/apiserver

COPY entrypoint.sh /entrypoint.sh
COPY postman /postman/
COPY templates /templates

ENTRYPOINT ["/entrypoint.sh"]
Expand Down
1 change: 1 addition & 0 deletions apiserver/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ BUILD_DATE=`date +%Y-%m-%d\ %H:%M`
VERSIONFILE="pkg/version/version.go"
VERSION="1.2.0"

set -e

if [ "$1" == "local" ] || [ "$1" == "docker" ]; then

Expand Down
273 changes: 156 additions & 117 deletions apiserver/cmd/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ func (s *Server) start(cfg *config.Config, adminPasswd string) {
routes = append(routes,
rest.Get(s.prefix, s.GetPaths),
rest.Get(s.prefix+"version", Version),
rest.Get(s.prefix+"validate", s.ValidateOAuth),
rest.Post(s.prefix+"authenticate", jwt.LoginHandler),
rest.Delete(s.prefix+"authenticate", s.Logout),
rest.Get(s.prefix+"check_token", s.CheckToken),
Expand Down Expand Up @@ -405,21 +406,11 @@ func (s *Server) start(cfg *config.Config, adminPasswd string) {
}
glog.Infof("Listening on %s", cfg.Port)

// internal admin server, currently only handling oauth registration
adminsrv := &http.Server{
Addr: ":" + cfg.AdminPort,
Handler: http.HandlerFunc(s.RegisterUserOauth),
}
glog.Infof("Admin server listening on %s", cfg.AdminPort)

stop := make(chan os.Signal, 2)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
go func() {
httpsrv.ListenAndServe()
}()
go func() {
adminsrv.ListenAndServe()
}()
<-stop

// Handle shutdown
Expand Down Expand Up @@ -537,6 +528,158 @@ func Version(w rest.ResponseWriter, r *rest.Request) {
w.WriteJson(fmt.Sprintf("%s %s", version.VERSION, version.BUILD_DATE))
}

func (s *Server) ValidateOAuth(w rest.ResponseWriter, r *rest.Request) {
glog.Info("Checking for OAuth2 cookie...")
oauth_cookie, err := r.Cookie("_oauth2_proxy") // cookie_segments[1]
if err != nil {
glog.Error(err)
w.WriteHeader(http.StatusUnauthorized)
return
}

oauth_host := "https://www." + s.Config.Domain
oauth_url := oauth_host + "/oauth2/userinfo"
glog.Infof("Validating OAuth2 cookie: %s", oauth_url)
req, err := http.NewRequest("GET", oauth_url, nil)
if err != nil {
glog.Error(err)
w.WriteHeader(http.StatusUnauthorized)
return
}

req.Header.Add("Host", "www." + s.Config.Domain)
req.AddCookie(oauth_cookie)

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
glog.Error(err)
w.WriteHeader(http.StatusUnauthorized)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
glog.Error("Got response from /userinfo that was not OK: " + string(resp.StatusCode))
w.WriteHeader(http.StatusUnauthorized)
return
}

body_bytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
glog.Error(err)
w.WriteHeader(http.StatusUnauthorized)
return
}

var oauth_fields map[string]string
err = json.Unmarshal(body_bytes, &oauth_fields)
if err != nil {
glog.Errorf("Failed to deserialize JSON: %s\n", oauth_fields)
w.WriteHeader(http.StatusUnauthorized)
return
}

oauth_email := oauth_fields["email"]
if oauth_email == "" {
glog.Warning("No OAuth fields found.") // + oauth_accessToken)
w.WriteHeader(http.StatusUnauthorized)
return
}


// Fallback to Email prefix if username not available
oauth_user := strings.Split(oauth_fields["preferredUsername"], "@")[0]
if oauth_user == "" {
oauth_user = strings.Split(oauth_email, "@")[0]
}

// Fallback to Username if full name not available
oauth_name := oauth_fields["name"]
if oauth_name == "" {
oauth_name = oauth_user
}

// TODO: Wire up accessToken? Is this needed for anything?
// oauth_accessToken := oauth_fields["accessToken"]

// TODO: Wire up otherTokens. This is needed for (at least) the MDF Forge Notebook
// Assign other tokens, if presented
/* oauth_otherTokenStr := oauth_fields["otherTokens"]
if oauth_otherTokensStr != "" {
tokens := make(map[string]string)
otherTokens := strings.Split(otherTokenStr, " ")
for _, kvpair := range otherTokens {
kv := strings.Split(kvpair, "=")
tokens[kv[0]] = kv[1]
}

// Write token(s) to user's home directory
err := s.writeAuthPayload(user, tokens)
if err != nil {
glog.Error(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
}
*/

// OAuth2 token is valid and contains everything we need, register account if necessary
glog.Infof("Creating/updating account for %s %s %s\n", oauth_user, oauth_email, oauth_name) //, oauth_accessToken)
// glog.V(4).Infof("Other tokens %s\n", otherTokens)


oauth_account := s.getAccountByEmail(oauth_email)
if oauth_account == nil {
act := api.Account{
Name: oauth_name,
Description: "Oauth shadow account", // Fetch this from other OAuth scope info?
Namespace: oauth_user,
EmailAddress: oauth_email,
Password: s.kube.RandomString(10),
Organization: "", // Fetch this from other OAuth scope info?
Created: time.Now().Unix(),
LastLogin: time.Now().Unix(),
NextURL: "", // TODO: rd,
}
act.Status = api.AccountStatusApproved

err := s.etcd.PutAccount(act.Namespace, &act, true)
if err != nil {
glog.Error(err)
w.WriteHeader(http.StatusInternalServerError)
return
}

err = s.setupAccount(&act)
if err != nil {
glog.Error(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
} else {
oauth_account.LastLogin = time.Now().Unix()
oauth_account.NextURL = "" // TODO: rd

err := s.etcd.PutAccount(oauth_account.Namespace, oauth_account, true)
if err != nil {
glog.Error(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
}

// Issue JWT
token, err := s.getTemporaryToken(oauth_user)
if err != nil {
glog.Error(err)
w.WriteHeader(http.StatusUnauthorized)
return
}

w.WriteJson(&token)
return
}

func (s *Server) CheckToken(w rest.ResponseWriter, r *rest.Request) {
// Basic token validation is handled by jwt middleware
userId := s.getUser(r)
Expand Down Expand Up @@ -744,10 +887,7 @@ func (s *Server) createLMABasicAuthSecret() error {
}

func (s *Server) setupAccount(account *api.Account) error {
_, err := s.kube.CreateNamespace(account.Namespace)
if err != nil {
return err
}
s.kube.CreateNamespace(account.Namespace)

// Create a PVC for this user's data
storageClass := s.Config.Kubernetes.StorageClass
Expand All @@ -764,19 +904,12 @@ func (s *Server) setupAccount(account *api.Account) error {
StorageQuota: s.Config.DefaultLimits.StorageDefault,
}
}
_, err = s.kube.CreateResourceQuota(account.Namespace,
s.kube.CreateResourceQuota(account.Namespace,
account.ResourceLimits.CPUMax,
account.ResourceLimits.MemoryMax)
if err != nil {
return err
}

_, err = s.kube.CreateLimitRange(account.Namespace,
s.kube.CreateLimitRange(account.Namespace,
account.ResourceLimits.CPUDefault,
account.ResourceLimits.MemoryDefault)
if err != nil {
return err
}

return nil
}
Expand Down Expand Up @@ -3214,97 +3347,3 @@ func (s *Server) writeAuthPayload(userId string, tokens map[string]string) error
}*/
return nil
}

// Register a user via oauth
func (s *Server) RegisterUserOauth(w http.ResponseWriter, r *http.Request) {

rd := r.FormValue("rd")
if rd == "" {
rd = "https://www." + s.domain + "/dashboard"
}

accessToken := r.Header.Get("X-Forwarded-Access-Token")
otherTokenStr := r.Header.Get("X-Forwarded-Other-Tokens")
email := r.Header.Get("X-Forwarded-Email")
user := r.Header.Get("X-Forwarded-User")

if accessToken == "" || email == "" || user == "" {
glog.Warning("No oauth header found")
w.WriteHeader(http.StatusUnauthorized)
return
}
tokens := make(map[string]string)
otherTokens := strings.Split(otherTokenStr, " ")
for _, kvpair := range otherTokens {
kv := strings.Split(kvpair, "=")
tokens[kv[0]] = kv[1]
}

err := s.writeAuthPayload(user, tokens)
if err != nil {
glog.Error(err)
w.WriteHeader(http.StatusInternalServerError)
return
}

glog.Infof("Creating/updating account for %s %s %s\n", user, email, accessToken)
glog.Infof("Other tokens %s\n", otherTokens)

account := s.getAccountByEmail(email)
if account == nil {
act := api.Account{
Name: user,
Description: "Oauth shadow account",
Namespace: user,
EmailAddress: email,
Password: s.kube.RandomString(10),
Organization: "",
Created: time.Now().Unix(),
LastLogin: time.Now().Unix(),
NextURL: rd,
}
act.Status = api.AccountStatusApproved

err := s.etcd.PutAccount(act.Namespace, &act, true)
if err != nil {
glog.Error(err)
w.WriteHeader(http.StatusInternalServerError)
return
}

err = s.setupAccount(&act)
if err != nil {
glog.Error(err)
w.WriteHeader(http.StatusInternalServerError)
return
}

} else {
account.LastLogin = time.Now().Unix()
account.NextURL = rd

err := s.etcd.PutAccount(account.Namespace, account, true)
if err != nil {
glog.Error(err)
w.WriteHeader(http.StatusInternalServerError)
return
}

}

token, err := s.getTemporaryToken(user)
if err != nil {
glog.Error(err)
w.WriteHeader(http.StatusOK)
return
}

glog.Infof("Setting Cookie\n")
//expiration := time.Now().Add(365 * 24 * time.Hour)
http.SetCookie(w, &http.Cookie{Name: "token", Value: token, Domain: s.domain, Path: "/"})
http.SetCookie(w, &http.Cookie{Name: "namespace", Value: user, Domain: s.domain, Path: "/"})

glog.Infof("Redirecting to %s\n", rd)
http.Redirect(w, r, rd, 301)
return
}
11 changes: 9 additions & 2 deletions apiserver/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ if [ "$1" = 'apiserver' ]; then
if [ -z "$ETCD_ADDR" ]; then
ETCD_ADDR="localhost:4001"
fi

if [ -z "$KUBERNETES_ADDR" ]; then
KUBERNETES_ADDR="https://localhost:6443"
fi
Expand Down Expand Up @@ -169,6 +169,13 @@ cat << EOF > /apiserver.json
}
EOF

echo -n "Waiting for etcd at $ETCD_ADDR..."
until $(curl -XGET --output /dev/null --silent --fail ${ETCD_ADDR}/version); do
echo -n "."
sleep 3
done
echo -e "\netcd is online: $ETCD_ADDR"

if [ -z "$SPEC_GIT_REPO" ]; then
SPEC_GIT_REPO=https://github.com/nds-org/ndslabs-specs
fi
Expand All @@ -185,7 +192,7 @@ EOF
umask 0

if [ -z "$TEST" ]; then
apiserver -conf /apiserver.json --logtostderr=true -v=1 -passwd $ADMIN_PASSWORD
apiserver -conf /apiserver.json --logtostderr=true -v=2 -passwd $ADMIN_PASSWORD
else
echo "Running binary with test/coverage instrumentation"
echo "Writing output to $VOLUME_PATH/coverage.out"
Expand Down
Loading