Skip to content

Commit

Permalink
Fixed some problems in access control testing (#505)
Browse files Browse the repository at this point in the history
  • Loading branch information
powerfooI authored Aug 15, 2024
1 parent e5e7622 commit 45e6b9e
Show file tree
Hide file tree
Showing 18 changed files with 562 additions and 65 deletions.
19 changes: 12 additions & 7 deletions charts/oceanbase-dashboard/templates/NOTES.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
Welcome to OceanBase dashboard!
___ ____
/ _ \ ___ ___ __ _ _ __ | __ ) __ _ ___ ___
| | | |/ __/ _ \/ _` | '_ \| _ \ / _` / __|/ _ \
Expand All @@ -10,18 +11,22 @@
| |_| | (_| \__ \ | | | |_) | (_) | (_| | | | (_| |
|____/ \__,_|___/_| |_|_.__/ \___/ \__,_|_| \__,_|

Welcome to OceanBase dashboard!

1. After installing the dashboard chart, you can use `port-forward` to expose the dashboard outside like:
1. [Temporary accessing] After installing the dashboard chart, you can use `port-forward` to expose the dashboard outside as following command:

> kubectl port-forward -n {{ .Release.Namespace }} services/oceanbase-dashboard-{{ .Release.Name }} 18081:80 --address 0.0.0.0

then you can visit the dashboard on http://$YOUR_SERVER_IP:18081
then you can visit the dashboard on http://$YOUR_SERVER_IP:18081 (Take 18081 as an example here)

2. For security reason, it is recommended to use a service to access the dashboard. By default the oceanbase-dashboard helm chart creates a service of type NodePort.
You can use the following command to get the nodePort of the dashboard service:

> export SERVICE_PORT_PORT=$(kubectl get -n {{ .Release.Namespace }} secret/oceanbase-dashboard-{{ .Release.Name }} -o jsonpath="{.spec.ports[?(@.name=='dashboard-backend')].nodePort}")

2. Use the following command to get password for default admin user
then you can visit the dashboard on http://$YOUR_SERVER_IP:$SERVICE_PORT_PORT

> echo $(kubectl get -n {{ .Release.Namespace }} secret {{ .Release.Name }}-user-credentials -o jsonpath='{.data.admin}' | base64 -d)
3. Login the dashboard with the following default account.
For users that log in for the first time, it is required to reset the password.

Log in as default account:
Username: admin
Password: <Get from the above command>
Password: <The password you set in values or "admin" by default>
2 changes: 1 addition & 1 deletion charts/oceanbase-dashboard/templates/user-credentials.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ metadata:
{{- include "oceanbase-dashboard.labels" . | nindent 4 }}
data:
{{- if empty (lookup "v1" "Secret" $.Release.Namespace (nospace (cat $.Release.Name "-user-credentials"))) }}
admin: {{ cat (sha256sum (.Values.adminPassword | default (randAlphaNum 16))) "admin" "0" "super admin" | b64enc }}
admin: {{ cat (sha256sum (.Values.adminPassword | default ("admin"))) "<|:SEP:|> admin <|:SEP:|> 0 <|:SEP:|> super admin" | b64enc }}
{{- else }}
admin: {{ (lookup "v1" "Secret" $.Release.Namespace (nospace (cat $.Release.Name "-user-credentials"))).data.admin }}
{{- end }}
Expand Down
4 changes: 2 additions & 2 deletions charts/oceanbase-dashboard/values.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
initCredentials: true
# base64 encoded password. If not set, the chart will generate it randomly
adminPassword:
# admin's password. If not set, the chart will generate it randomly
adminPassword: admin

userCredentials:
userNamespace:
Expand Down
166 changes: 125 additions & 41 deletions internal/dashboard/business/ac/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import (
"crypto/sha256"
"encoding/hex"
"os"
"strconv"
"sort"
"strings"
"time"

Expand All @@ -26,6 +26,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

acmodel "github.com/oceanbase/ob-operator/internal/dashboard/model/ac"
"github.com/oceanbase/ob-operator/internal/dashboard/model/param"
httpErr "github.com/oceanbase/ob-operator/pkg/errors"
"github.com/oceanbase/ob-operator/pkg/k8s/client"
)
Expand Down Expand Up @@ -57,7 +58,7 @@ func GetAccount(ctx context.Context, username string) (*acmodel.Account, error)
}

func Enforce(_ context.Context, username string, policy *acmodel.Policy) (bool, error) {
return enforcer.Enforce(username, policy.Object, policy.Action)
return enforcer.Enforce(username, policy.ComposeDomainObject(), string(policy.Action))
}

func ValidateAccount(ctx context.Context, username, password string) (*acmodel.Account, error) {
Expand All @@ -81,15 +82,66 @@ func ValidateAccount(ctx context.Context, username, password string) (*acmodel.A
account.Roles = roles
}

now := time.Now().Unix()
err = updateUserCredentials(ctx, credentials, username, account.password, account.Nickname, strconv.FormatInt(now, 10), account.Description)
if err != nil {
logrus.WithError(err).Warn("failed to update user credentials")
if account.LastLoginAt != nil && account.LastLoginAt.Unix() != 0 {
// The account has logged in before
enforcer.accMu.Lock()
defer enforcer.accMu.Unlock()
// Update latest login time
now := time.Now().Unix()
up := &acmodel.UpdateAccountCreds{
Username: username,
AccountCreds: acmodel.AccountCreds{
EncryptedPassword: account.password,
Nickname: account.Nickname,
LastLoginAtUnix: now,
Description: account.Description,
},
}
err = updateUserCredentials(ctx, credentials, up)
if err != nil {
logrus.WithError(err).Warn("failed to update user credentials")
}
}

return &account.Account, nil
}

// ResetAccountPassword resets account's own password.
func ResetAccountPassword(ctx context.Context, username string, resetParam *param.ResetPasswordParam) (*acmodel.Account, error) {
credentials, err := getDashboardUserCredentials(ctx)
if err != nil {
return nil, err
}
account, err := fetchAccount(credentials, username)
if err != nil {
return nil, err
}
if account.LastLoginAt != nil && account.LastLoginAt.Unix() != 0 {
bts := sha256.Sum256([]byte(resetParam.OldPassword))
sha256EncodedPwd := hex.EncodeToString(bts[:])
if account.password != sha256EncodedPwd {
return nil, httpErr.NewBadRequest("password is incorrect")
}
}
newBts := sha256.Sum256([]byte(resetParam.Password))
newEncryptedPwd := hex.EncodeToString(newBts[:])

up := &acmodel.UpdateAccountCreds{
Username: username,
AccountCreds: acmodel.AccountCreds{
EncryptedPassword: newEncryptedPwd,
Nickname: account.Nickname,
LastLoginAtUnix: time.Now().Unix(),
Description: account.Description,
},
}
err = updateUserCredentials(ctx, credentials, up)
if err != nil {
return nil, httpErr.NewInternal("failed to update user credentials")
}
return nil, nil
}

func CreateAccount(ctx context.Context, param *acmodel.CreateAccountParam) (*acmodel.Account, error) {
enforcer.accMu.Lock()
defer enforcer.accMu.Unlock()
Expand All @@ -101,14 +153,14 @@ func CreateAccount(ctx context.Context, param *acmodel.CreateAccountParam) (*acm
return nil, httpErr.NewBadRequest("username already exists")
}

roles, err := enforcer.GetFilteredPolicy(0, param.Roles...)
if err != nil {
return nil, err
}
if len(roles) != len(param.Roles) {
return nil, httpErr.NewBadRequest("role does not exist")
}
for _, role := range param.Roles {
policies, err := enforcer.GetFilteredPolicy(0, role)
if err != nil {
return nil, err
}
if len(policies) == 0 {
return nil, httpErr.NewBadRequest("role does not exist: " + role)
}
ok, err := enforcer.AddRoleForUser(param.Username, role)
if err != nil {
return nil, err
Expand All @@ -119,7 +171,15 @@ func CreateAccount(ctx context.Context, param *acmodel.CreateAccountParam) (*acm
}
bts := sha256.Sum256([]byte(param.Password))
sha256EncodedPwd := hex.EncodeToString(bts[:])
err = updateUserCredentials(ctx, credentials, param.Username, sha256EncodedPwd, param.Nickname, "0", param.Description)
up := &acmodel.UpdateAccountCreds{
Username: param.Username,
AccountCreds: acmodel.AccountCreds{
EncryptedPassword: sha256EncodedPwd,
Nickname: param.Nickname,
Description: param.Description,
},
}
err = updateUserCredentials(ctx, credentials, up)
if err != nil {
return nil, httpErr.NewInternal("failed to update user credentials")
}
Expand All @@ -146,9 +206,11 @@ func PatchAccount(ctx context.Context, username string, param *acmodel.PatchAcco
return nil, err
}
if len(param.Roles) > 0 {
_, err := enforcer.GetFilteredPolicy(0, param.Roles...)
if err != nil {
return nil, err
for _, role := range param.Roles {
_, err := enforcer.GetFilteredPolicy(0, role)
if err != nil {
return nil, httpErr.NewBadRequest("role does not exist: " + role)
}
}
for _, role := range acc.Roles {
ok, err := enforcer.DeleteRoleForUser(username, role.Name)
Expand All @@ -171,8 +233,9 @@ func PatchAccount(ctx context.Context, username string, param *acmodel.PatchAcco
}

accountChanged := false
if param.Password != "" && acc.password != param.Password {
acc.password = param.Password
if param.Password != "" {
bts := sha256.Sum256([]byte(param.Password))
acc.password = hex.EncodeToString(bts[:])
accountChanged = true
}
if param.Nickname != "" && acc.Nickname != param.Nickname {
Expand All @@ -184,7 +247,16 @@ func PatchAccount(ctx context.Context, username string, param *acmodel.PatchAcco
accountChanged = true
}
if accountChanged {
err = updateUserCredentials(ctx, credentials, username, acc.password, acc.Nickname, strconv.FormatInt(time.Now().Unix(), 10), acc.Description)
up := &acmodel.UpdateAccountCreds{
Username: username,
AccountCreds: acmodel.AccountCreds{
EncryptedPassword: acc.password,
Nickname: acc.Nickname,
LastLoginAtUnix: acc.LastLoginAt.Unix(),
Description: acc.Description,
},
}
err = updateUserCredentials(ctx, credentials, up)
if err != nil {
return nil, httpErr.NewInternal("failed to update user credentials")
}
Expand Down Expand Up @@ -220,7 +292,10 @@ func DeleteAccount(ctx context.Context, username string) (*acmodel.Account, erro
if err != nil {
return nil, err
}
err = updateUserCredentials(ctx, credentials, username, "", "", "0", "")
err = updateUserCredentials(ctx, credentials, &acmodel.UpdateAccountCreds{
Username: username,
Delete: true,
})
if err != nil {
return nil, httpErr.NewInternal("failed to update user credentials")
}
Expand All @@ -240,31 +315,26 @@ func getDashboardUserCredentials(c context.Context) (*v1.Secret, error) {
return clt.ClientSet.CoreV1().Secrets(ns).Get(c, secretName, metav1.GetOptions{})
}

func updateUserCredentials(c context.Context, credentials *v1.Secret, username, password, nickname, lastLoginAtUnix, description string) error {
if password == "" {
delete(credentials.Data, username)
func updateUserCredentials(c context.Context, credentials *v1.Secret, up *acmodel.UpdateAccountCreds) error {
if up.Delete {
delete(credentials.Data, up.Username)
} else {
credentials.Data[username] = []byte(strings.Join([]string{password, nickname, lastLoginAtUnix, description}, " "))
credentials.Data[up.Username] = []byte(up.ToLine())
}
clt := client.GetClient()
_, err := clt.ClientSet.CoreV1().Secrets(os.Getenv("USER_NAMESPACE")).Update(c, credentials, metav1.UpdateOptions{})
return err
}

// pwd nickname lastLogin description
func fetchAccount(credentials *v1.Secret, username string) (*account, error) {
if _, ok := credentials.Data[username]; !ok {
return nil, httpErr.NewBadRequest("username or password is incorrect")
}
infoLine := string(credentials.Data[username])

parts := strings.SplitN(infoLine, " ", 4)
if len(parts) != 4 {
return nil, httpErr.NewInternal("User credentials file is corrupted: invalid format")
}
ts, err := strconv.ParseInt(parts[2], 10, 64)
creds, err := acmodel.NewAccountCreds(infoLine)
if err != nil {
return nil, httpErr.NewInternal("User credentials file is corrupted: last login time is not a valid timestamp")
return nil, httpErr.NewInternal(err.Error())
}
roles, err := getAccountRoles(username)
if err != nil {
Expand All @@ -273,16 +343,22 @@ func fetchAccount(credentials *v1.Secret, username string) (*account, error) {
if len(roles) == 0 {
return nil, httpErr.NewInternal("User credentials file is corrupted: user has no role")
}
lastLoginAt := time.Unix(ts, 0)
var lastLoginAt *time.Time
if creds.LastLoginAtUnix != 0 {
tmpNow := time.Unix(creds.LastLoginAtUnix, 0)
lastLoginAt = &tmpNow
}
needReset := creds.LastLoginAtUnix == 0
return &account{
Account: acmodel.Account{
Username: username,
Nickname: parts[1],
LastLoginAt: &lastLoginAt,
Description: parts[3],
Nickname: creds.Nickname,
LastLoginAt: lastLoginAt,
Description: creds.Description,
Roles: roles,
NeedReset: needReset,
},
password: parts[0],
password: creds.EncryptedPassword,
}, nil
}

Expand All @@ -299,6 +375,9 @@ func fetchAccounts(ctx context.Context) ([]acmodel.Account, error) {
}
accounts = append(accounts, account.Account)
}
sort.SliceStable(accounts, func(i, j int) bool {
return accounts[i].Username < accounts[j].Username
})
return accounts, nil
}

Expand All @@ -318,10 +397,15 @@ func getAccountRoles(username string) ([]acmodel.Role, error) {
}
policies := make([]acmodel.Policy, 0, len(policyLines))
for _, line := range policyLines {
policies = append(policies, acmodel.Policy{
Object: acmodel.Object(line[1]),
Action: acmodel.Action(line[2]),
})
if line[1] == "*" {
policies = append(policies, acmodel.NewPolicy("*", "*", line[2]))
} else {
parts := strings.Split(line[1], "/")
if len(parts) != 2 {
return nil, httpErr.NewInternal("corrupted policy" + strings.Join(line, " "))
}
policies = append(policies, acmodel.NewPolicy(parts[0], parts[1], line[2]))
}
}
modelRoles = append(modelRoles, acmodel.Role{
Name: role,
Expand Down
7 changes: 7 additions & 0 deletions internal/dashboard/business/ac/account_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ import (
)

var _ = Describe("Access Control", Ordered, ContinueOnFailure, func() {
It("GetFilteredPolicies", func() {
roles, err := enforcer.GetFilteredPolicy(0, "admin")
Expect(err).To(BeNil())
Expect(roles).To(HaveLen(1))
GinkgoLogr.Info("roles", "roles", roles)
})

It("GetPolicies", func() {
ps, err := enforcer.GetPolicy()
Expect(err).To(BeNil())
Expand Down
Loading

0 comments on commit 45e6b9e

Please sign in to comment.