Skip to content

Commit

Permalink
Merge pull request #336 from nds-org/external-auth
Browse files Browse the repository at this point in the history
Testing / enhancing oauth integration
  • Loading branch information
bodom0015 authored Mar 12, 2021
2 parents 6491aeb + 2af1231 commit a007ac1
Show file tree
Hide file tree
Showing 12 changed files with 268 additions and 150 deletions.
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

0 comments on commit a007ac1

Please sign in to comment.