401 lines
11 KiB
Go
401 lines
11 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"sort"
|
|
|
|
"github.com/influxdata/influxdb/v2/chronograf"
|
|
"github.com/influxdata/influxdb/v2/chronograf/oauth2"
|
|
"github.com/influxdata/influxdb/v2/chronograf/organizations"
|
|
"golang.org/x/net/context"
|
|
)
|
|
|
|
type meLinks struct {
|
|
Self string `json:"self"` // Self link mapping to this resource
|
|
}
|
|
|
|
type meResponse struct {
|
|
*chronograf.User
|
|
Links meLinks `json:"links"`
|
|
Organizations []chronograf.Organization `json:"organizations"`
|
|
CurrentOrganization *chronograf.Organization `json:"currentOrganization,omitempty"`
|
|
}
|
|
|
|
type noAuthMeResponse struct {
|
|
Links meLinks `json:"links"`
|
|
}
|
|
|
|
func newNoAuthMeResponse() noAuthMeResponse {
|
|
return noAuthMeResponse{
|
|
Links: meLinks{
|
|
Self: "/chronograf/v1/me",
|
|
},
|
|
}
|
|
}
|
|
|
|
// If new user response is nil, return an empty meResponse because it
|
|
// indicates authentication is not needed
|
|
func newMeResponse(usr *chronograf.User, org string) meResponse {
|
|
base := "/chronograf/v1"
|
|
name := "me"
|
|
if usr != nil {
|
|
base = fmt.Sprintf("/chronograf/v1/organizations/%s/users", org)
|
|
name = PathEscape(fmt.Sprintf("%d", usr.ID))
|
|
}
|
|
|
|
return meResponse{
|
|
User: usr,
|
|
Links: meLinks{
|
|
Self: fmt.Sprintf("%s/%s", base, name),
|
|
},
|
|
}
|
|
}
|
|
|
|
// TODO: This Scheme value is hard-coded temporarily since we only currently
|
|
// support OAuth2. This hard-coding should be removed whenever we add
|
|
// support for other authentication schemes.
|
|
func getScheme(ctx context.Context) (string, error) {
|
|
return "oauth2", nil
|
|
}
|
|
|
|
func getPrincipal(ctx context.Context) (oauth2.Principal, error) {
|
|
principal, ok := ctx.Value(oauth2.PrincipalKey).(oauth2.Principal)
|
|
if !ok {
|
|
return oauth2.Principal{}, fmt.Errorf("token not found")
|
|
}
|
|
|
|
return principal, nil
|
|
}
|
|
|
|
func getValidPrincipal(ctx context.Context) (oauth2.Principal, error) {
|
|
p, err := getPrincipal(ctx)
|
|
if err != nil {
|
|
return p, err
|
|
}
|
|
if p.Subject == "" {
|
|
return oauth2.Principal{}, fmt.Errorf("token not found")
|
|
}
|
|
if p.Issuer == "" {
|
|
return oauth2.Principal{}, fmt.Errorf("token not found")
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
type meRequest struct {
|
|
// Organization is the OrganizationID
|
|
Organization string `json:"organization"`
|
|
}
|
|
|
|
// UpdateMe changes the user's current organization on the JWT and responds
|
|
// with the same semantics as Me
|
|
func (s *Service) UpdateMe(auth oauth2.Authenticator) func(http.ResponseWriter, *http.Request) {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
serverCtx := serverContext(ctx)
|
|
principal, err := auth.Validate(ctx, r)
|
|
if err != nil {
|
|
s.Logger.Error(fmt.Sprintf("Invalid principal: %v", err))
|
|
Error(w, http.StatusForbidden, "invalid principal", s.Logger)
|
|
return
|
|
}
|
|
var req meRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
invalidJSON(w, s.Logger)
|
|
return
|
|
}
|
|
|
|
// validate that the organization exists
|
|
org, err := s.Store.Organizations(serverCtx).Get(serverCtx, chronograf.OrganizationQuery{ID: &req.Organization})
|
|
if err != nil {
|
|
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
|
|
return
|
|
}
|
|
|
|
// validate that user belongs to organization
|
|
ctx = context.WithValue(ctx, organizations.ContextKey, req.Organization)
|
|
|
|
p, err := getValidPrincipal(ctx)
|
|
if err != nil {
|
|
invalidData(w, err, s.Logger)
|
|
return
|
|
}
|
|
if p.Organization == "" {
|
|
defaultOrg, err := s.Store.Organizations(serverCtx).DefaultOrganization(serverCtx)
|
|
if err != nil {
|
|
unknownErrorWithMessage(w, err, s.Logger)
|
|
return
|
|
}
|
|
p.Organization = defaultOrg.ID
|
|
}
|
|
scheme, err := getScheme(ctx)
|
|
if err != nil {
|
|
invalidData(w, err, s.Logger)
|
|
return
|
|
}
|
|
_, err = s.Store.Users(ctx).Get(ctx, chronograf.UserQuery{
|
|
Name: &p.Subject,
|
|
Provider: &p.Issuer,
|
|
Scheme: &scheme,
|
|
})
|
|
if err == chronograf.ErrUserNotFound {
|
|
// If the user was not found, check to see if they are a super admin. If
|
|
// they are, add them to the organization.
|
|
u, err := s.Store.Users(serverCtx).Get(serverCtx, chronograf.UserQuery{
|
|
Name: &p.Subject,
|
|
Provider: &p.Issuer,
|
|
Scheme: &scheme,
|
|
})
|
|
if err != nil {
|
|
Error(w, http.StatusForbidden, err.Error(), s.Logger)
|
|
return
|
|
}
|
|
|
|
if !u.SuperAdmin {
|
|
// Since a user is not a part of this organization and not a super admin,
|
|
// we should tell them that they are Forbidden (403) from accessing this resource
|
|
Error(w, http.StatusForbidden, chronograf.ErrUserNotFound.Error(), s.Logger)
|
|
return
|
|
}
|
|
|
|
// If the user is a super admin give them an admin role in the
|
|
// requested organization.
|
|
u.Roles = append(u.Roles, chronograf.Role{
|
|
Organization: org.ID,
|
|
Name: org.DefaultRole,
|
|
})
|
|
if err := s.Store.Users(serverCtx).Update(serverCtx, u); err != nil {
|
|
unknownErrorWithMessage(w, err, s.Logger)
|
|
return
|
|
}
|
|
} else if err != nil {
|
|
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
|
|
return
|
|
}
|
|
|
|
// TODO: change to principal.CurrentOrganization
|
|
principal.Organization = req.Organization
|
|
|
|
if err := auth.Authorize(ctx, w, principal); err != nil {
|
|
Error(w, http.StatusInternalServerError, err.Error(), s.Logger)
|
|
return
|
|
}
|
|
|
|
ctx = context.WithValue(ctx, oauth2.PrincipalKey, principal)
|
|
|
|
s.Me(w, r.WithContext(ctx))
|
|
}
|
|
}
|
|
|
|
// Me does a findOrCreate based on the username in the context
|
|
func (s *Service) Me(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
if !s.UseAuth {
|
|
// If there's no authentication, return an empty user
|
|
res := newNoAuthMeResponse()
|
|
encodeJSON(w, http.StatusOK, res, s.Logger)
|
|
return
|
|
}
|
|
|
|
p, err := getValidPrincipal(ctx)
|
|
if err != nil {
|
|
invalidData(w, err, s.Logger)
|
|
return
|
|
}
|
|
scheme, err := getScheme(ctx)
|
|
if err != nil {
|
|
invalidData(w, err, s.Logger)
|
|
return
|
|
}
|
|
|
|
ctx = context.WithValue(ctx, organizations.ContextKey, p.Organization)
|
|
serverCtx := serverContext(ctx)
|
|
|
|
defaultOrg, err := s.Store.Organizations(serverCtx).DefaultOrganization(serverCtx)
|
|
if err != nil {
|
|
unknownErrorWithMessage(w, err, s.Logger)
|
|
return
|
|
}
|
|
|
|
if p.Organization == "" {
|
|
p.Organization = defaultOrg.ID
|
|
}
|
|
|
|
usr, err := s.Store.Users(serverCtx).Get(serverCtx, chronograf.UserQuery{
|
|
Name: &p.Subject,
|
|
Provider: &p.Issuer,
|
|
Scheme: &scheme,
|
|
})
|
|
if err != nil && err != chronograf.ErrUserNotFound {
|
|
unknownErrorWithMessage(w, err, s.Logger)
|
|
return
|
|
}
|
|
|
|
// user exists
|
|
if usr != nil {
|
|
superAdmin := s.mapPrincipalToSuperAdmin(p)
|
|
if superAdmin && !usr.SuperAdmin {
|
|
usr.SuperAdmin = superAdmin
|
|
err := s.Store.Users(serverCtx).Update(serverCtx, usr)
|
|
if err != nil {
|
|
unknownErrorWithMessage(w, err, s.Logger)
|
|
return
|
|
}
|
|
}
|
|
|
|
currentOrg, err := s.Store.Organizations(serverCtx).Get(serverCtx, chronograf.OrganizationQuery{ID: &p.Organization})
|
|
if err == chronograf.ErrOrganizationNotFound {
|
|
// The intent is to force a the user to go through another auth flow
|
|
Error(w, http.StatusForbidden, "user's current organization was not found", s.Logger)
|
|
return
|
|
}
|
|
if err != nil {
|
|
unknownErrorWithMessage(w, err, s.Logger)
|
|
return
|
|
}
|
|
|
|
orgs, err := s.usersOrganizations(serverCtx, usr)
|
|
if err != nil {
|
|
unknownErrorWithMessage(w, err, s.Logger)
|
|
return
|
|
}
|
|
|
|
res := newMeResponse(usr, currentOrg.ID)
|
|
res.Organizations = orgs
|
|
res.CurrentOrganization = currentOrg
|
|
encodeJSON(w, http.StatusOK, res, s.Logger)
|
|
return
|
|
}
|
|
|
|
// Because we didnt find a user, making a new one
|
|
user := &chronograf.User{
|
|
Name: p.Subject,
|
|
Provider: p.Issuer,
|
|
// TODO: This Scheme value is hard-coded temporarily since we only currently
|
|
// support OAuth2. This hard-coding should be removed whenever we add
|
|
// support for other authentication schemes.
|
|
Scheme: scheme,
|
|
// TODO(desa): this needs a better name
|
|
SuperAdmin: s.newUsersAreSuperAdmin(),
|
|
}
|
|
|
|
superAdmin := s.mapPrincipalToSuperAdmin(p)
|
|
if superAdmin {
|
|
user.SuperAdmin = superAdmin
|
|
}
|
|
|
|
roles, err := s.mapPrincipalToRoles(serverCtx, p)
|
|
if err != nil {
|
|
Error(w, http.StatusInternalServerError, err.Error(), s.Logger)
|
|
return
|
|
}
|
|
|
|
if !superAdmin && len(roles) == 0 {
|
|
Error(w, http.StatusForbidden, "This Chronograf is private. To gain access, you must be explicitly added by an administrator.", s.Logger)
|
|
return
|
|
}
|
|
|
|
// If the user is a superadmin, give them a role in the default organization
|
|
if user.SuperAdmin {
|
|
hasDefaultOrgRole := false
|
|
for _, role := range roles {
|
|
if role.Organization == defaultOrg.ID {
|
|
hasDefaultOrgRole = true
|
|
break
|
|
}
|
|
}
|
|
if !hasDefaultOrgRole {
|
|
roles = append(roles, chronograf.Role{
|
|
Name: defaultOrg.DefaultRole,
|
|
Organization: defaultOrg.ID,
|
|
})
|
|
}
|
|
}
|
|
|
|
user.Roles = roles
|
|
|
|
newUser, err := s.Store.Users(serverCtx).Add(serverCtx, user)
|
|
if err != nil {
|
|
msg := fmt.Errorf("error storing user %s: %v", user.Name, err)
|
|
unknownErrorWithMessage(w, msg, s.Logger)
|
|
return
|
|
}
|
|
|
|
orgs, err := s.usersOrganizations(serverCtx, newUser)
|
|
if err != nil {
|
|
unknownErrorWithMessage(w, err, s.Logger)
|
|
return
|
|
}
|
|
currentOrg, err := s.Store.Organizations(serverCtx).Get(serverCtx, chronograf.OrganizationQuery{ID: &p.Organization})
|
|
if err != nil {
|
|
unknownErrorWithMessage(w, err, s.Logger)
|
|
return
|
|
}
|
|
res := newMeResponse(newUser, currentOrg.ID)
|
|
res.Organizations = orgs
|
|
res.CurrentOrganization = currentOrg
|
|
encodeJSON(w, http.StatusOK, res, s.Logger)
|
|
}
|
|
|
|
func (s *Service) firstUser() bool {
|
|
serverCtx := serverContext(context.Background())
|
|
numUsers, err := s.Store.Users(serverCtx).Num(serverCtx)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
return numUsers == 0
|
|
}
|
|
func (s *Service) newUsersAreSuperAdmin() bool {
|
|
// It's not necessary to enforce that the first user is superAdmin here, since
|
|
// superAdminNewUsers defaults to true, but there's nothing else in the
|
|
// application that dictates that it must be true.
|
|
// So for that reason, we kept this here for now. We've discussed the
|
|
// future possibility of allowing users to override default values via CLI and
|
|
// this case could possibly happen then.
|
|
if s.firstUser() {
|
|
return true
|
|
}
|
|
serverCtx := serverContext(context.Background())
|
|
cfg, err := s.Store.Config(serverCtx).Get(serverCtx)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return cfg.Auth.SuperAdminNewUsers
|
|
}
|
|
|
|
func (s *Service) usersOrganizations(ctx context.Context, u *chronograf.User) ([]chronograf.Organization, error) {
|
|
if u == nil {
|
|
// TODO(desa): better error
|
|
return nil, fmt.Errorf("user was nil")
|
|
}
|
|
|
|
orgIDs := map[string]bool{}
|
|
for _, role := range u.Roles {
|
|
orgIDs[role.Organization] = true
|
|
}
|
|
|
|
orgs := []chronograf.Organization{}
|
|
for orgID := range orgIDs {
|
|
org, err := s.Store.Organizations(ctx).Get(ctx, chronograf.OrganizationQuery{ID: &orgID})
|
|
|
|
// There can be race conditions between deleting a organization and the me query
|
|
if err == chronograf.ErrOrganizationNotFound {
|
|
continue
|
|
}
|
|
|
|
// Any other error should cause an error to be returned
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
orgs = append(orgs, *org)
|
|
}
|
|
|
|
sort.Slice(orgs, func(i, j int) bool {
|
|
return orgs[i].ID < orgs[j].ID
|
|
})
|
|
|
|
return orgs, nil
|
|
}
|