switch to token auth for admin to support account image urls

This commit is contained in:
Roland Osborne 2022-06-06 15:18:45 -07:00
parent 23ff473286
commit ab9935fac7
22 changed files with 178 additions and 119 deletions

View File

@ -84,8 +84,13 @@ paths:
- admin
description: Get node configuration. Access granted to admin username and password.
operationId: get-node-config
security:
- basicAuth: []
parameters:
- name: token
in: query
description: token for admin access
required: true
schema:
type: string
responses:
'200':
description: success
@ -102,8 +107,13 @@ paths:
- admin
description: Set node config. Access granted to admin username and password.
operationId: set-node-config
security:
- basicAuth: []
parameters:
- name: token
in: query
description: token for admin access
required: true
schema:
type: string
responses:
'200':
description: success
@ -118,8 +128,13 @@ paths:
- admin
description: Get list of accounts hosted on node. Access granted to admin username and password.
operationId: get-node-accounts
security:
- basicAuth: []
parameters:
- name: token
in: query
description: token for admin access
required: true
schema:
type: string
responses:
'200':
description: successful operation
@ -140,8 +155,13 @@ paths:
- admin
description: Gernerate a url for creating a new account. Access granted to admin username and password.
operationId: add-node-account
security:
- basicAuth: []
parameters:
- name: token
in: query
description: token for admin access
required: true
schema:
type: string
responses:
'201':
description: success
@ -160,8 +180,6 @@ paths:
- admin
description: Get profile image of node account.
operationId: get-node-account-image
security:
- basicAuth: []
parameters:
- name: accountId
in: path
@ -169,6 +187,12 @@ paths:
required: true
schema:
type: string
- name: token
in: query
description: token for admin access
required: true
schema:
type: string
responses:
'200':
description: successful operation
@ -199,6 +223,12 @@ paths:
required: true
schema:
type: string
- name: token
in: query
description: token for admin access
required: true
schema:
type: string
responses:
'200':
description: successful operation

View File

@ -10,8 +10,8 @@ import (
func AddNodeAccount(w http.ResponseWriter, r *http.Request) {
if err := AdminLogin(r); err != nil {
ErrResponse(w, http.StatusUnauthorized, err)
if code, err := ParamAdminToken(r); err != nil {
ErrResponse(w, code, err)
return
}

View File

@ -22,8 +22,8 @@ func GetNodeAccountImage(w http.ResponseWriter, r *http.Request) {
return
}
if err := AdminLogin(r); err != nil {
ErrResponse(w, http.StatusUnauthorized, err)
if code, err := ParamAdminToken(r); err != nil {
ErrResponse(w, code, err)
return
}

View File

@ -7,8 +7,8 @@ import (
func GetNodeAccounts(w http.ResponseWriter, r *http.Request) {
if err := AdminLogin(r); err != nil {
ErrResponse(w, http.StatusUnauthorized, err)
if code, err := ParamAdminToken(r); err != nil {
ErrResponse(w, code, err)
return
}

View File

@ -7,8 +7,8 @@ import (
func GetNodeConfig(w http.ResponseWriter, r *http.Request) {
// validate login
if err := AdminLogin(r); err != nil {
ErrResponse(w, http.StatusUnauthorized, err)
if code, err := ParamAdminToken(r); err != nil {
ErrResponse(w, code, err)
return
}

View File

@ -20,8 +20,8 @@ func RemoveNodeAccount(w http.ResponseWriter, r *http.Request) {
return
}
if err := AdminLogin(r); err != nil {
ErrResponse(w, http.StatusUnauthorized, err)
if code, err := ParamAdminToken(r); err != nil {
ErrResponse(w, code, err)
return
}

View File

@ -20,8 +20,8 @@ func SetNodeAccount(w http.ResponseWriter, r *http.Request) {
return
}
if res = AdminLogin(r); res != nil {
ErrResponse(w, http.StatusUnauthorized, res)
if code, res := ParamAdminToken(r); res != nil {
ErrResponse(w, code, res)
return
}

View File

@ -10,8 +10,8 @@ import (
func SetNodeConfig(w http.ResponseWriter, r *http.Request) {
// validate login
if err := AdminLogin(r); err != nil {
ErrResponse(w, http.StatusUnauthorized, err)
if code, err := ParamAdminToken(r); err != nil {
ErrResponse(w, code, err)
return
}

View File

@ -20,18 +20,14 @@ func SetNodeStatus(w http.ResponseWriter, r *http.Request) {
return
}
username, password, res := BasicCredentials(r);
if res != nil || username != "admin" {
LogMsg("invalid credenitals");
token := r.FormValue("token")
if token == "" {
w.WriteHeader(http.StatusBadRequest)
return
}
err = store.DB.Transaction(func(tx *gorm.DB) error {
if res := tx.Create(&store.Config{ConfigId: CONFIG_USERNAME, StrValue: username}).Error; res != nil {
return res
}
if res := tx.Create(&store.Config{ConfigId: CONFIG_PASSWORD, BinValue: password}).Error; res != nil {
if res := tx.Create(&store.Config{ConfigId: CONFIG_TOKEN, StrValue: token}).Error; res != nil {
return res
}
if res := tx.Create(&store.Config{ConfigId: CONFIG_CONFIGURED, BoolValue: true}).Error; res != nil {

View File

@ -11,33 +11,6 @@ import (
"databag/internal/store"
)
func AdminLogin(r *http.Request) error {
// extract request auth
username, password, ok := r.BasicAuth()
if !ok || username == "" || password == "" {
return errors.New("invalid credentials")
}
// nothing to do if not configured
if !getBoolConfigValue(CONFIG_CONFIGURED, false) {
return errors.New("node not configured")
}
// compare username
if getStrConfigValue(CONFIG_USERNAME, "") != username {
return errors.New("admin username error")
}
// compare password
p := getBinConfigValue(CONFIG_PASSWORD, nil);
if bcrypt.CompareHashAndPassword(p, []byte(password)) != nil {
return errors.New("admin password error")
}
return nil
}
func AccountLogin(r *http.Request) (*store.Account, error) {
// extract request auth
@ -77,6 +50,28 @@ func BearerAccountToken(r *http.Request) (*store.AccountToken, error) {
return &accountToken, nil
}
func ParamAdminToken(r *http.Request) (int, error) {
// parse authentication token
token := r.FormValue("token")
if token == "" {
return http.StatusUnauthorized, errors.New("token not set");
}
// nothing to do if not configured
if !getBoolConfigValue(CONFIG_CONFIGURED, false) {
return http.StatusUnauthorized, errors.New("node not configured")
}
// compare password
value := getStrConfigValue(CONFIG_TOKEN, "");
if (value != token) {
return http.StatusUnauthorized, errors.New("invalid admin token")
}
return http.StatusOK, nil;
}
func ParamAgentToken(r *http.Request, detail bool) (*store.Account, int, error) {
// parse authentication token

View File

@ -9,8 +9,7 @@ import (
const CONFIG_OPENACCESS = "open_access"
const CONFIG_ACCOUNTLIMIT = "account_limit"
const CONFIG_CONFIGURED = "configured"
const CONFIG_USERNAME = "username"
const CONFIG_PASSWORD = "password"
const CONFIG_TOKEN = "token"
const CONFIG_DOMAIN = "domain"
const CONFIG_STORAGE = "storage"
const CONFIG_ASSETPATH = "asset_path"

View File

@ -34,8 +34,7 @@ func TestMain(m *testing.M) {
}
// claim server
r, w, _ = NewRequest("PUT", "/admin/status", nil)
SetCredentials(r, "admin:pass");
r, w, _ = NewRequest("PUT", "/admin/status?token=pass", nil)
SetNodeStatus(w, r)
if ReadResponse(w, nil) != nil {
panic("failed to claim server")
@ -55,16 +54,14 @@ func TestMain(m *testing.M) {
// config server
config := NodeConfig{Domain: "databag.coredb.org", AccountLimit: 1024, OpenAccess: true, AccountStorage: 4096}
r, w, _ = NewRequest("PUT", "/admin/config", &config)
SetBasicAuth(r, "admin:pass")
r, w, _ = NewRequest("PUT", "/admin/config?token=pass", &config)
SetNodeConfig(w, r)
if ReadResponse(w, nil) != nil {
panic("failed to set config")
}
// check config
r, w, _ = NewRequest("GET", "/admin/config", nil)
SetBasicAuth(r, "admin:pass")
r, w, _ = NewRequest("GET", "/admin/config?token=pass", nil)
GetNodeConfig(w, r)
var check NodeConfig
if ReadResponse(w, &check) != nil {

View File

@ -620,10 +620,9 @@ func AddTestAccount(username string) (guid string, token string, err error) {
var login = username + ":pass"
// get account token
if r, w, err= NewRequest("POST", "/admin/accounts", nil); err != nil {
if r, w, err= NewRequest("POST", "/admin/accounts?token=pass", nil); err != nil {
return
}
SetBasicAuth(r, "admin:pass")
AddNodeAccount(w, r)
if err = ReadResponse(w, &access); err != nil {
return

View File

@ -0,0 +1,4 @@
export function AccountItem() {
return <div>ACCOUNT</div>
}

View File

@ -1,7 +1,8 @@
import { DashboardWrapper, SettingsButton, AddButton, SettingsLayout } from './Dashboard.styled';
import { Button, Modal, Input, InputNumber, Space } from 'antd';
import { SettingOutlined, UserAddOutlined } from '@ant-design/icons';
import { Button, Modal, Input, InputNumber, Space, List } from 'antd';
import { SettingOutlined, UserAddOutlined, ReloadOutlined } from '@ant-design/icons';
import { useDashboard } from './useDashboard.hook';
import { AccountItem } from './AccountItem/AccountItem';
export function Dashboard({ password, config }) {
@ -13,6 +14,10 @@ export function Dashboard({ password, config }) {
<div class="container">
<div class="header">
<div class="label">Accounts</div>
<div class="settings">
<SettingsButton type="text" size="small" icon={<ReloadOutlined />}
onClick={() => actions.getAccounts()}></SettingsButton>
</div>
<div class="settings">
<SettingsButton type="text" size="small" icon={<SettingOutlined />}
onClick={() => actions.setShowSettings(true)}></SettingsButton>
@ -21,10 +26,20 @@ export function Dashboard({ password, config }) {
<AddButton type="text" size="large" icon={<UserAddOutlined />}></AddButton>
</div>
</div>
<div class="body">
<List
locale={{ emptyText: '' }}
itemLayout="horizontal"
dataSource={state.accounts}
loading={state.loading}
renderItem={item => (<AccountItem item={item} />)}
/>
</div>
</div>
<Modal title="Settings" visible={state.showSettings} centered
okText="Save" onOk={() => actions.onSaveSettings()} onCancel={() => actions.setShowSettings(false)}>
okText="Save" onOk={() => actions.setSettings()} onCancel={() => actions.setShowSettings(false)}>
<SettingsLayout direction="vertical">
<div class="host">
<div>Federated Host:&nbsp;</div>

View File

@ -17,7 +17,7 @@ export const DashboardWrapper = styled.div`
border-radius: 4px;
max-width: 500px;
width: 50%;
}
max-height: 80%;
.header {
color: #444444;
@ -27,6 +27,11 @@ export const DashboardWrapper = styled.div`
border-bottom: 1px solid #444444;
}
.body {
min-height: 0;
overflow: auto;
}
.label {
padding-right: 8px;
padding-left: 4px;
@ -45,6 +50,7 @@ export const DashboardWrapper = styled.div`
justify-content: flex-end;
flex-grow: 1;
}
}
`;
export const AddButton = styled(Button)`

View File

@ -1,5 +1,6 @@
import { useState, useEffect } from 'react';
import { setNodeConfig } from 'api/setNodeConfig';
import { getNodeAccounts } from 'api/getNodeAccounts';
export function useDashboard(password, config) {
@ -8,20 +9,14 @@ export function useDashboard(password, config) {
storage: null,
showSettings: false,
busy: false,
loading: false,
accounts: [],
});
const updateState = (value) => {
setState((s) => ({ ...s, ...value }));
}
useEffect(() => {
let storage = config.accountStorage / 1073741824;
if (storage > 1) {
storage = Math.ceil(storage);
}
updateState({ host: config.domain, storage: storage });
}, []);
const actions = {
setHost: (value) => {
updateState({ host: value });
@ -32,7 +27,7 @@ export function useDashboard(password, config) {
setShowSettings: (value) => {
updateState({ showSettings: value });
},
onSaveSettings: async () => {
setSettings: async () => {
if (!state.busy) {
updateState({ busy: true });
try {
@ -47,8 +42,31 @@ export function useDashboard(password, config) {
updateState({ busy: false });
}
},
getAccounts: async () => {
if (!state.loading) {
updateState({ loading: true });
try {
let accounts = await getNodeAccounts(password);
updateState({ accounts });
}
catch(err) {
console.log(err);
window.alert(err);
}
updateState({ loading: false });
}
},
};
useEffect(() => {
let storage = config.accountStorage / 1073741824;
if (storage > 1) {
storage = Math.ceil(storage);
}
updateState({ host: config.domain, storage: storage });
actions.getAccounts();
}, []);
return { state, actions };
}

View File

@ -42,7 +42,8 @@ export function useAdmin() {
setAccess: async () => {
try {
await setNodeStatus(state.token);
updateState({ access: state.token, unclaimed: false });
let config = await getNodeConfig(state.token);
updateState({ access: state.token, unclaimed: false, config });
}
catch(err) {
console.log(err);

View File

@ -0,0 +1,8 @@
import { checkResponse, fetchWithTimeout } from './fetchUtil';
export async function getNodeAccounts(token) {
let accounts = await fetchWithTimeout(`/admin/accounts?token=${token}`, { method: 'GET' });
checkResponse(accounts);
return await accounts.json();
}

View File

@ -1,10 +1,7 @@
import { checkResponse, fetchWithTimeout } from './fetchUtil';
var base64 = require('base-64');
export async function getNodeConfig(password) {
let headers = new Headers()
headers.append('Authorization', 'Basic ' + base64.encode("admin:" + password));
let config = await fetchWithTimeout(`/admin/config`, { method: 'GET', headers });
export async function getNodeConfig(token) {
let config = await fetchWithTimeout(`/admin/config?token=${token}`, { method: 'GET' });
checkResponse(config);
return await config.json();
}

View File

@ -1,11 +1,8 @@
import { checkResponse, fetchWithTimeout } from './fetchUtil';
var base64 = require('base-64');
export async function setNodeConfig(password, config) {
export async function setNodeConfig(token, config) {
let body = JSON.stringify(config);
let headers = new Headers()
headers.append('Authorization', 'Basic ' + base64.encode("admin:" + password));
let settings = await fetchWithTimeout(`/admin/config`, { method: 'PUT', headers, body });
let settings = await fetchWithTimeout(`/admin/config?token=${token}`, { method: 'PUT', body });
checkResponse(settings);
}

View File

@ -1,10 +1,7 @@
import { checkResponse, fetchWithTimeout } from './fetchUtil';
var base64 = require('base-64');
export async function setNodeStatus(password) {
let headers = new Headers()
headers.append('Credentials', 'Basic ' + base64.encode("admin:" + password));
let status = await fetchWithTimeout(`/admin/status`, { method: 'PUT', headers });
export async function setNodeStatus(token) {
let status = await fetchWithTimeout(`/admin/status?token=${token}`, { method: 'PUT' });
checkResponse(status);
}