diff --git a/api/authn/authn.go b/api/authn/authn.go index bd867110ae..572dd547eb 100644 --- a/api/authn/authn.go +++ b/api/authn/authn.go @@ -1,6 +1,6 @@ // Package authn provides AuthN API over HTTP(S) /* - * Copyright (c) 2018-2023, NVIDIA CORPORATION. All rights reserved. + * Copyright (c) 2018-2024, NVIDIA CORPORATION. All rights reserved. */ package authn @@ -61,9 +61,9 @@ func DeleteUser(bp api.BaseParams, userID string) error { // Authorize a user and return a user token in case of success. // The token expires in `expire` time. If `expire` is `nil` the expiration // time is set by AuthN (default AuthN expiration time is 24 hours) -func LoginUser(bp api.BaseParams, userID, pass, clusterID string, expire *time.Duration) (token *TokenMsg, err error) { +func LoginUser(bp api.BaseParams, userID, pass string, expire *time.Duration) (token *TokenMsg, err error) { bp.Method = http.MethodPost - rec := LoginMsg{Password: pass, ExpiresIn: expire, ClusterID: clusterID} + rec := LoginMsg{Password: pass, ExpiresIn: expire} reqParams := api.AllocRp() defer api.FreeRp(reqParams) { @@ -135,8 +135,8 @@ func GetRegisteredClusters(bp api.BaseParams, spec CluACL) ([]*CluACL, error) { clusters := &RegisteredClusters{} _, err := reqParams.DoReqAny(clusters) - rec := make([]*CluACL, 0, len(clusters.M)) - for _, clu := range clusters.M { + rec := make([]*CluACL, 0, len(clusters.Clusters)) + for _, clu := range clusters.Clusters { rec = append(rec, clu) } less := func(i, j int) bool { return rec[i].ID < rec[j].ID } @@ -174,7 +174,7 @@ func GetAllRoles(bp api.BaseParams) ([]*Role, error) { roles := make([]*Role, 0) _, err := reqParams.DoReqAny(&roles) - less := func(i, j int) bool { return roles[i].ID < roles[j].ID } + less := func(i, j int) bool { return roles[i].Name < roles[j].Name } sort.Slice(roles, less) return roles, err } @@ -240,7 +240,7 @@ func UpdateRole(bp api.BaseParams, roleSpec *Role) error { defer api.FreeRp(reqParams) { reqParams.BaseParams = bp - reqParams.Path = apc.URLPathRoles.Join(roleSpec.ID) + reqParams.Path = apc.URLPathRoles.Join(roleSpec.Name) reqParams.Body = msg reqParams.Header = http.Header{cos.HdrContentType: []string{cos.ContentJSON}} } diff --git a/api/authn/entity.go b/api/authn/entity.go index 77b7d413c4..197ead24c6 100644 --- a/api/authn/entity.go +++ b/api/authn/entity.go @@ -1,6 +1,6 @@ // Package authn provides AuthN API over HTTP(S) /* - * Copyright (c) 2018-2022, NVIDIA CORPORATION. All rights reserved. + * Copyright (c) 2018-2024, NVIDIA CORPORATION. All rights reserved. */ package authn @@ -18,37 +18,39 @@ const ( type ( User struct { - ID string `json:"id"` - Password string `json:"pass,omitempty"` - Roles []string `json:"roles"` - ClusterACLs []*CluACL `json:"clusters"` - BucketACLs []*BckACL `json:"buckets"` // list of buckets with special permissions + ID string `json:"id"` + Password string `json:"pass,omitempty"` + Roles []*Role `json:"roles"` } + CluACL struct { ID string `json:"id"` Alias string `json:"alias,omitempty"` Access apc.AccessAttrs `json:"perm,string,omitempty"` URLs []string `json:"urls,omitempty"` } + BckACL struct { Bck cmn.Bck `json:"bck"` Access apc.AccessAttrs `json:"perm,string"` } + TokenMsg struct { Token string `json:"token"` } + LoginMsg struct { Password string `json:"password"` ExpiresIn *time.Duration `json:"expires_in"` - ClusterID string `json:"cluster_id"` } + RegisteredClusters struct { - M map[string]*CluACL `json:"clusters,omitempty"` + Clusters map[string]*CluACL `json:"clusters,omitempty"` } + Role struct { - ID string `json:"name"` - Desc string `json:"desc"` - Roles []string `json:"roles"` + Name string `json:"name"` + Description string `json:"desc"` ClusterACLs []*CluACL `json:"clusters"` BucketACLs []*BckACL `json:"buckets"` IsAdmin bool `json:"admin"` @@ -60,10 +62,10 @@ type ( ////////// // IsAdmin returns true if the user is an admin or super-user, -// i.e. the user has the full access to everything. -func (uInfo *User) IsAdmin() bool { - for _, r := range uInfo.Roles { - if r == AdminRole { +// i.e. the user has full access to everything. +func (u *User) IsAdmin() bool { + for _, r := range u.Roles { + if r.Name == AdminRole { return true } } diff --git a/api/env/authn.go b/api/env/authn.go index f03d39387e..f01a9201a2 100644 --- a/api/env/authn.go +++ b/api/env/authn.go @@ -1,6 +1,6 @@ // Package env contains environment variables /* - * Copyright (c) 2018-2022, NVIDIA CORPORATION. All rights reserved. + * Copyright (c) 2018-2024, NVIDIA CORPORATION. All rights reserved. */ package env @@ -9,24 +9,26 @@ package env var ( AuthN = struct { - Enabled string - URL string - TokenFile string - ConfDir string - LogDir string - LogLevel string - Port string - TTL string - UseHTTPS string + Enabled string + URL string + TokenFile string + ConfDir string + LogDir string + LogLevel string + Port string + TTL string + UseHTTPS string + AdminPassword string }{ - Enabled: "AIS_AUTHN_ENABLED", - URL: "AIS_AUTHN_URL", - TokenFile: "AIS_AUTHN_TOKEN_FILE", // fully qualified - ConfDir: "AIS_AUTHN_CONF_DIR", // contains AuthN config and tokens DB - LogDir: "AIS_AUTHN_LOG_DIR", - LogLevel: "AIS_AUTHN_LOG_LEVEL", - Port: "AIS_AUTHN_PORT", - TTL: "AIS_AUTHN_TTL", - UseHTTPS: "AIS_AUTHN_USE_HTTPS", + Enabled: "AIS_AUTHN_ENABLED", + URL: "AIS_AUTHN_URL", + TokenFile: "AIS_AUTHN_TOKEN_FILE", // fully qualified + ConfDir: "AIS_AUTHN_CONF_DIR", // contains AuthN config and tokens DB + LogDir: "AIS_AUTHN_LOG_DIR", + LogLevel: "AIS_AUTHN_LOG_LEVEL", + Port: "AIS_AUTHN_PORT", + TTL: "AIS_AUTHN_TTL", + UseHTTPS: "AIS_AUTHN_USE_HTTPS", + AdminPassword: "AIS_AUTHN_ADMIN_PASSWORD", } ) diff --git a/cmd/authn/hserv.go b/cmd/authn/hserv.go index 51e92d43ce..df1429870b 100644 --- a/cmd/authn/hserv.go +++ b/cmd/authn/hserv.go @@ -249,16 +249,6 @@ func (h *hserv) httpUserGet(w http.ResponseWriter, r *http.Request) { return } uInfo.Password = "" - clus, err := h.mgr.clus() - if err != nil { - cmn.WriteErr(w, r, err) - return - } - for _, clu := range uInfo.ClusterACLs { - if cInfo, ok := clus[clu.ID]; ok { - clu.Alias = cInfo.Alias - } - } writeJSON(w, uInfo, "user info") } @@ -412,7 +402,7 @@ func (h *hserv) httpSrvGet(w http.ResponseWriter, r *http.Request) { return } cluList = &authn.RegisteredClusters{ - M: map[string]*authn.CluACL{clu.ID: clu}, + Clusters: map[string]*authn.CluACL{clu.ID: clu}, } } else { clus, err := h.mgr.clus() @@ -420,7 +410,7 @@ func (h *hserv) httpSrvGet(w http.ResponseWriter, r *http.Request) { cmn.WriteErr(w, r, err, http.StatusInternalServerError) return } - cluList = &authn.RegisteredClusters{M: clus} + cluList = &authn.RegisteredClusters{Clusters: clus} } writeJSON(w, cluList, "auth") } diff --git a/cmd/authn/mgr.go b/cmd/authn/mgr.go index 9727e84398..80b4655e28 100644 --- a/cmd/authn/mgr.go +++ b/cmd/authn/mgr.go @@ -9,10 +9,12 @@ import ( "errors" "fmt" "net/http" + "os" "time" "github.com/NVIDIA/aistore/api/apc" "github.com/NVIDIA/aistore/api/authn" + "github.com/NVIDIA/aistore/api/env" "github.com/NVIDIA/aistore/cmd/authn/tok" "github.com/NVIDIA/aistore/cmn" "github.com/NVIDIA/aistore/cmn/cos" @@ -100,9 +102,6 @@ func (m *mgr) updateUser(userID string, updateReq *authn.User) error { if len(updateReq.Roles) != 0 { uInfo.Roles = updateReq.Roles } - uInfo.ClusterACLs = mergeClusterACLs(uInfo.ClusterACLs, updateReq.ClusterACLs, "") - uInfo.BucketACLs = mergeBckACLs(uInfo.BucketACLs, updateReq.BucketACLs, "") - return m.db.Set(usersCollection, userID, uInfo) } @@ -112,18 +111,6 @@ func (m *mgr) lookupUser(userID string) (*authn.User, error) { if err != nil { return nil, err } - - // update ACLs with roles's ones - for _, role := range uInfo.Roles { - rInfo := &authn.Role{} - err := m.db.Get(rolesCollection, role, rInfo) - if err != nil { - continue - } - uInfo.ClusterACLs = mergeClusterACLs(uInfo.ClusterACLs, rInfo.ClusterACLs, "") - uInfo.BucketACLs = mergeBckACLs(uInfo.BucketACLs, rInfo.BucketACLs, "") - } - return uInfo, nil } @@ -148,18 +135,18 @@ func (m *mgr) userList() (map[string]*authn.User, error) { // Registers a new role func (m *mgr) addRole(info *authn.Role) error { - if info.ID == "" { + if info.Name == "" { return errors.New("role name is undefined") } if info.IsAdmin { return fmt.Errorf("only built-in roles can have %q permissions", adminUserID) } - _, err := m.db.GetString(rolesCollection, info.ID) + _, err := m.db.GetString(rolesCollection, info.Name) if err == nil { - return fmt.Errorf("role %q already exists", info.ID) + return fmt.Errorf("role %q already exists", info.Name) } - return m.db.Set(rolesCollection, info.ID, info) + return m.db.Set(rolesCollection, info.Name, info) } // Deletes an existing role @@ -181,11 +168,8 @@ func (m *mgr) updateRole(role string, updateReq *authn.Role) error { return cos.NewErrNotFound(m, "role "+role) } - if updateReq.Desc != "" { - rInfo.Desc = updateReq.Desc - } - if len(updateReq.Roles) != 0 { - rInfo.Roles = updateReq.Roles + if updateReq.Description != "" { + rInfo.Description = updateReq.Description } rInfo.ClusterACLs = mergeClusterACLs(rInfo.ClusterACLs, updateReq.ClusterACLs, "") rInfo.BucketACLs = mergeBckACLs(rInfo.BucketACLs, updateReq.BucketACLs, "") @@ -229,12 +213,12 @@ func (m *mgr) createRolesForCluster(clu *authn.CluACL) { if err := m.db.Get(rolesCollection, uid, rInfo); err == nil { continue } - rInfo.ID = uid + rInfo.Name = uid cluName := clu.ID if clu.Alias != "" { cluName += "[" + clu.Alias + "]" } - rInfo.Desc = fmt.Sprintf(pr.desc, cluName) + rInfo.Description = fmt.Sprintf(pr.desc, cluName) rInfo.ClusterACLs = []*authn.CluACL{ {ID: clu.ID, Access: pr.perms}, } @@ -372,6 +356,8 @@ func (m *mgr) issueToken(userID, pwd string, msg *authn.LoginMsg) (string, error token string uInfo = &authn.User{} cid string + cluACLs []*authn.CluACL + bckACLs []*authn.BckACL ) err = m.db.Get(usersCollection, userID, uInfo) @@ -382,27 +368,11 @@ func (m *mgr) issueToken(userID, pwd string, msg *authn.LoginMsg) (string, error if !isSamePassword(pwd, uInfo.Password) { return "", errInvalidCredentials } - if !uInfo.IsAdmin() { - if msg.ClusterID == "" { - return "", fmt.Errorf("Couldn't issue token for %q: cluster ID not set", userID) - } - cid = m.cluLookup(msg.ClusterID, msg.ClusterID) - if cid == "" { - return "", cos.NewErrNotFound(m, "cluster "+msg.ClusterID) - } - uInfo.ClusterACLs = mergeClusterACLs(make([]*authn.CluACL, 0, len(uInfo.ClusterACLs)), uInfo.ClusterACLs, cid) - uInfo.BucketACLs = mergeBckACLs(make([]*authn.BckACL, 0, len(uInfo.BucketACLs)), uInfo.BucketACLs, cid) - } // update ACLs with roles's ones for _, role := range uInfo.Roles { - rInfo := &authn.Role{} - err := m.db.Get(rolesCollection, role, rInfo) - if err != nil { - continue - } - uInfo.ClusterACLs = mergeClusterACLs(uInfo.ClusterACLs, rInfo.ClusterACLs, cid) - uInfo.BucketACLs = mergeBckACLs(uInfo.BucketACLs, rInfo.BucketACLs, cid) + cluACLs = mergeClusterACLs(cluACLs, role.ClusterACLs, cid) + bckACLs = mergeBckACLs(bckACLs, role.BucketACLs, cid) } // generate token @@ -424,8 +394,8 @@ func (m *mgr) issueToken(userID, pwd string, msg *authn.LoginMsg) (string, error if uInfo.IsAdmin() { token, err = tok.IssueAdminJWT(expires, userID, Conf.Server.Secret) } else { - m.fixClusterIDs(uInfo.ClusterACLs) - token, err = tok.IssueJWT(expires, userID, uInfo.BucketACLs, uInfo.ClusterACLs, Conf.Server.Secret) + m.fixClusterIDs(cluACLs) + token, err = tok.IssueJWT(expires, userID, bckACLs, cluACLs, Conf.Server.Secret) } return token, err } @@ -513,21 +483,33 @@ func isSamePassword(password, hashed string) bool { func initializeDB(driver kvdb.Driver) error { users, err := driver.List(usersCollection, "") if err != nil || len(users) != 0 { - // return on erros or when DB is already initialized + // Return on errors or when DB is already initialized return err } + + // Create the admin role role := &authn.Role{ - ID: authn.AdminRole, - Desc: "AuthN administrator", - IsAdmin: true, + Name: authn.AdminRole, + Description: "AuthN administrator", + IsAdmin: true, } + if err := driver.Set(rolesCollection, authn.AdminRole, role); err != nil { return err } + + // Get the admin password from the environment variable or use the default + password := os.Getenv(env.AuthN.AdminPassword) + if password == "" { + password = adminUserPass + } + + // Create the admin user su := &authn.User{ ID: adminUserID, - Password: encryptPassword(adminUserPass), - Roles: []string{authn.AdminRole}, + Password: encryptPassword(password), + Roles: []*authn.Role{role}, } + return driver.Set(usersCollection, adminUserID, su) } diff --git a/cmd/authn/unit_test.go b/cmd/authn/unit_test.go index 6f12b8251f..d3611f600e 100644 --- a/cmd/authn/unit_test.go +++ b/cmd/authn/unit_test.go @@ -1,6 +1,6 @@ // Package authn is authentication server for AIStore. /* - * Copyright (c) 2018-2022, NVIDIA CORPORATION. All rights reserved. + * Copyright (c) 2018-2024, NVIDIA CORPORATION. All rights reserved. */ package main @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/NVIDIA/aistore/api/apc" "github.com/NVIDIA/aistore/api/authn" "github.com/NVIDIA/aistore/cmd/authn/tok" "github.com/NVIDIA/aistore/cmn" @@ -17,8 +18,15 @@ import ( ) var ( - users = []string{"user1", "user2", "user3"} - passs = []string{"pass2", "pass1", "passs"} + users = []string{"user1", "user2", "user3"} + passs = []string{"pass2", "pass1", "passs"} + guestRole = &authn.Role{ + Name: GuestRole, + Description: "Read-only access to buckets", + ClusterACLs: []*authn.CluACL{ + {ID: "test-clu-id", Access: apc.AccessRO}, + }, + } ) func init() { @@ -30,49 +38,46 @@ func init() { func createUsers(mgr *mgr, t *testing.T) { for idx := range users { - user := &authn.User{ID: users[idx], Password: passs[idx], Roles: []string{GuestRole}} + user := &authn.User{ID: users[idx], Password: passs[idx], Roles: []*authn.Role{guestRole}} err := mgr.addUser(user) if err != nil { - t.Errorf("Failed to create a user %s: %v", users[idx], err) + t.Errorf("Failed to create user %s: %v", users[idx], err) } } srvUsers, err := mgr.userList() tassert.CheckFatal(t, err) - if len(srvUsers) != len(users)+1 { - t.Errorf("User count mismatch. Found %d users instead of %d", len(srvUsers), len(users)+1) + expectedUsersCount := len(users) + 1 // including the admin user + if len(srvUsers) != expectedUsersCount { + t.Errorf("User count mismatch. Found %d users instead of %d", len(srvUsers), expectedUsersCount) } for _, username := range users { - _, ok := srvUsers[username] - if !ok { + if _, ok := srvUsers[username]; !ok { t.Errorf("User %q not found", username) } } } func deleteUsers(mgr *mgr, skipNotExist bool, t *testing.T) { - var err error for _, username := range users { - err = mgr.delUser(username) - if err != nil { - if !cos.IsErrNotFound(err) || !skipNotExist { - t.Errorf("Failed to delete user %s: %v", username, err) - } + err := mgr.delUser(username) + if err != nil && (!cos.IsErrNotFound(err) || !skipNotExist) { + t.Errorf("Failed to delete user %s: %v", username, err) } } } func testInvalidUser(mgr *mgr, t *testing.T) { - user := &authn.User{ID: users[0], Password: passs[1], Roles: []string{GuestRole}} + user := &authn.User{ID: users[0], Password: passs[1], Roles: []*authn.Role{guestRole}} err := mgr.addUser(user) if err == nil { - t.Errorf("User with the existing name %s was created: %v", users[0], err) + t.Errorf("User with the existing name %s was created", users[0]) } nonexisting := "someuser" err = mgr.delUser(nonexisting) if err == nil { - t.Errorf("Non-existing user %s was deleted: %v", nonexisting, err) + t.Errorf("Non-existing user %s was deleted", nonexisting) } } @@ -81,15 +86,16 @@ func testUserDelete(mgr *mgr, t *testing.T) { username = "newuser" userpass = "newpass" ) - user := &authn.User{ID: username, Password: userpass, Roles: []string{GuestRole}} + user := &authn.User{ID: username, Password: userpass, Roles: []*authn.Role{guestRole}} err := mgr.addUser(user) if err != nil { - t.Errorf("Failed to create a user %s: %v", username, err) + t.Errorf("Failed to create user %s: %v", username, err) } srvUsers, err := mgr.userList() tassert.CheckFatal(t, err) - if len(srvUsers) != len(users)+2 { - t.Errorf("Expected %d users but found %d", len(users)+2, len(srvUsers)) + expectedUsersCount := len(users) + 2 // including the admin user and the new user + if len(srvUsers) != expectedUsersCount { + t.Errorf("Expected %d users but found %d", expectedUsersCount, len(srvUsers)) } clu := authn.CluACL{ @@ -102,7 +108,7 @@ func testUserDelete(mgr *mgr, t *testing.T) { } defer mgr.delCluster(clu.ID) - loginMsg := &authn.LoginMsg{ClusterID: clu.Alias} + loginMsg := &authn.LoginMsg{} token, err := mgr.issueToken(username, userpass, loginMsg) if err != nil || token == "" { t.Errorf("Failed to generate token for %s: %v", username, err) @@ -114,12 +120,13 @@ func testUserDelete(mgr *mgr, t *testing.T) { } srvUsers, err = mgr.userList() tassert.CheckFatal(t, err) - if len(srvUsers) != len(users)+1 { - t.Errorf("Expected %d users but found %d", len(users)+1, len(srvUsers)) + expectedUsersCount = len(users) + 1 // including the admin user + if len(srvUsers) != expectedUsersCount { + t.Errorf("Expected %d users but found %d", expectedUsersCount, len(srvUsers)) } token, err = mgr.issueToken(username, userpass, loginMsg) if err == nil { - t.Errorf("Token issued for deleted user %s: %v", username, token) + t.Errorf("Token issued for deleted user %s: %v", username, token) } else if err != errInvalidCredentials { t.Errorf("Invalid error: %v", err) } @@ -127,7 +134,7 @@ func testUserDelete(mgr *mgr, t *testing.T) { func TestManager(t *testing.T) { driver := mock.NewDBDriver() - // NOTE: new manager initailizes users DB and adds a default user as a Guest + // NOTE: new manager initializes users DB and adds a default user as a Guest mgr, err := newMgr(driver) tassert.CheckError(t, err) createUsers(mgr, t) @@ -164,14 +171,14 @@ func TestToken(t *testing.T) { // correct user creds shortExpiration := 2 * time.Second - loginMsg := &authn.LoginMsg{ClusterID: clu.Alias, ExpiresIn: &shortExpiration} + loginMsg := &authn.LoginMsg{ExpiresIn: &shortExpiration} token, err = mgr.issueToken(users[1], passs[1], loginMsg) if err != nil || token == "" { t.Errorf("Failed to generate token for %s: %v", users[1], err) } info, err := tok.DecryptToken(token, secret) if err != nil { - t.Fatalf("Failed to decript token %v: %v", token, err) + t.Fatalf("Failed to decrypt token %v: %v", token, err) } if info.UserID != users[1] { t.Errorf("Invalid user %s returned for token of %s", info.UserID, users[1]) diff --git a/docs/authn.md b/docs/authn.md index 0eb4ca6416..c97a1048aa 100644 --- a/docs/authn.md +++ b/docs/authn.md @@ -56,7 +56,7 @@ To deploy an AIS cluster with AuthN enabled, follow these steps: make kill clean ``` -> **Note:** When deploying AIStore with AuthN, an admin user is created by default with admin privileges. The default password for the admin user is `admin`. Be sure to [change this password](../cmd/authn/const.go#L22) before starting the server, as it cannot be updated after deployment. +> **Note:** When deploying AIStore with AuthN, an admin user is created by default with admin privileges. The default password for the admin user is `admin`. You can override it by setting the environment variable `AIS_AUTHN_ADMIN_PASSWORD` before deployment, as the password cannot be updated after deployment. For a list of other important environment variables, refer to the [Environment and Configuration](#environment-and-configuration) section. 2. Deploy the cluster with AuthN enabled: ```sh