add apparmor hat transitions
This commit is contained in:
parent
d8ec9bb883
commit
0fb7171198
14 changed files with 336 additions and 33 deletions
16
.vscode/c_cpp_properties.json
vendored
Normal file
16
.vscode/c_cpp_properties.json
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Linux",
|
||||
"includePath": [
|
||||
"${workspaceFolder}/**"
|
||||
],
|
||||
"defines": [],
|
||||
"compilerPath": "/usr/bin/gcc",
|
||||
"cStandard": "gnu17",
|
||||
"cppStandard": "gnu++17",
|
||||
"intelliSenseMode": "linux-gcc-x64"
|
||||
}
|
||||
],
|
||||
"version": 4
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
<h2>お姫様の成績がアップデートされました!</h2>
|
||||
<h2><a href="https://yoake.yumechi.jp/#canvas-grades">最近の採点</h2>
|
||||
<h2><a href="https://yoake.yumechi.jp/#canvas-grades">最近の採点</a></h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
|
@ -17,10 +17,9 @@
|
|||
<tr>
|
||||
<td>{{ .SubmissionUserName }}</td>
|
||||
<td>{{ .CourseCode }}</td>
|
||||
<td><a href="{{.AssignmentURL}}"">{{ .Name }}</a></td>
|
||||
<td><a href="{{.AssignmentURL}}">{{ .Name }}</a></td>
|
||||
<td>{{if eq .Due " -" }}No Due{{else}}{{.Due}}{{end}}</td>
|
||||
<td>{{if .GradeHidden }}(hidden){{else}}{{if le .Score -0.01}}(not graded){{else}}{{ .Score | sprintf "%.2f"
|
||||
}} ({{ .Grade }}){{end}} /{{ .PossiblePoints | sprintf "%.2f" }} {{end}}</td>
|
||||
<td>{{if .GradeHidden }}(hidden){{else}}{{if le .Score -0.01}}(not graded){{else}}{{ .Score | sprintf "%.2f" }} ({{ .Grade }}){{end}} /{{ .PossiblePoints | sprintf "%.2f" }} {{end}}</td>
|
||||
<td>{{ .GradedAt }} </td>
|
||||
<td>{{ .PostedAt }}</td>
|
||||
</tr>
|
||||
|
|
|
@ -3,8 +3,10 @@ package main
|
|||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/eternal-flame-AD/yoake/config"
|
||||
"github.com/eternal-flame-AD/yoake/internal/apparmor"
|
||||
"github.com/eternal-flame-AD/yoake/internal/comm"
|
||||
"github.com/eternal-flame-AD/yoake/internal/db"
|
||||
"github.com/eternal-flame-AD/yoake/server"
|
||||
|
@ -37,10 +39,45 @@ func init() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func changeHat() {
|
||||
profile := config.Config().Listen.AppArmor.Serve
|
||||
if profile != "" {
|
||||
token, err := apparmor.GetMagicToken()
|
||||
if err != nil {
|
||||
log.Panicf("failed to get apparmor magic token: %v", err)
|
||||
}
|
||||
if err := apparmor.ChangeHat(profile, token); err != nil {
|
||||
log.Panicf("failed to change apparmor hat: %v", err)
|
||||
} else {
|
||||
log.Printf("changed apparmor hat to %s", profile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
listen := config.Config().Listen
|
||||
if listen.Ssl.Use {
|
||||
log.Fatalln(server.Server.StartTLS(listen.Addr, listen.Ssl.Cert, listen.Ssl.Key))
|
||||
var sslCertBytes, sslKeyBytes []byte
|
||||
apparmor.ExecuteInHat(listen.AppArmor.SSL, func() {
|
||||
var err error
|
||||
sslCertBytes, err = os.ReadFile(listen.Ssl.Cert)
|
||||
if err != nil {
|
||||
log.Panicf("failed to read ssl cert: %v", err)
|
||||
}
|
||||
sslKeyBytes, err = os.ReadFile(listen.Ssl.Key)
|
||||
if err != nil {
|
||||
log.Panicf("failed to read ssl key: %v", err)
|
||||
}
|
||||
}, true)
|
||||
if listen.AppArmor.SSL != "" {
|
||||
// defensive programming, try read ssl key
|
||||
if _, err := os.ReadFile(listen.Ssl.Key); err == nil {
|
||||
log.Panicf("AppArmor profile set for SSL but I could still read %v!", listen.Ssl.Key)
|
||||
}
|
||||
}
|
||||
|
||||
log.Fatalln(server.Server.StartTLS(listen.Addr, sslCertBytes, sslKeyBytes))
|
||||
} else {
|
||||
log.Fatalln(server.Server.Start(listen.Addr))
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
listen:
|
||||
addr: :80
|
||||
apparmor:
|
||||
ssl: ssl
|
||||
ssl:
|
||||
use: off
|
||||
cert: "fullchain.pem"
|
||||
|
|
|
@ -8,8 +8,12 @@ import (
|
|||
type C struct {
|
||||
Hosts map[string]string
|
||||
Listen struct {
|
||||
Addr string
|
||||
Ssl struct {
|
||||
Addr string
|
||||
AppArmor struct {
|
||||
Serve string
|
||||
SSL string
|
||||
}
|
||||
Ssl struct {
|
||||
Use bool
|
||||
Cert string
|
||||
Key string
|
||||
|
|
|
@ -6,12 +6,13 @@ make verify
|
|||
make build
|
||||
or exit 2
|
||||
|
||||
if ! diff etc/yoake-server.service /etc/systemd/system/yoake-server.service
|
||||
sudo cp etc/yoake-server.service /etc/systemd/system/yoake-server.service
|
||||
or exit 2
|
||||
|
||||
sudo cp etc/yoake-server.service /etc/systemd/system/yoake-server.service
|
||||
or exit 2
|
||||
|
||||
sudo systemctl daemon-reload
|
||||
or exit 2
|
||||
sudo systemctl daemon-reload
|
||||
or exit 2
|
||||
end
|
||||
|
||||
sudo systemctl stop yoake-server.service
|
||||
|
||||
|
|
65
internal/apparmor/api.go
Normal file
65
internal/apparmor/api.go
Normal file
|
@ -0,0 +1,65 @@
|
|||
package apparmor
|
||||
|
||||
import (
|
||||
// #cgo LDFLAGS: -lapparmor
|
||||
// #include "./apparmor.h"
|
||||
"C"
|
||||
)
|
||||
import (
|
||||
"runtime"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func ChangeHat(subprofile string, magicToken uint64) error {
|
||||
var ret uintptr
|
||||
if subprofile != "" {
|
||||
subProfileC := C.CString(subprofile)
|
||||
defer C.free(unsafe.Pointer(subProfileC))
|
||||
ret = uintptr(C.go_aa_change_hat(subProfileC, C.ulong(magicToken)))
|
||||
} else {
|
||||
ret = uintptr(C.go_aa_change_hat(nil, C.ulong(magicToken)))
|
||||
}
|
||||
|
||||
if ret != 0 {
|
||||
return syscall.Errno(ret)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ExecuteInHat(subprofile string, fn func(), lockThread bool) error {
|
||||
if subprofile == "" {
|
||||
fn()
|
||||
return nil
|
||||
}
|
||||
if lockThread {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
}
|
||||
token, err := GetMagicToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ChangeHat(subprofile, token); err != nil {
|
||||
return err
|
||||
}
|
||||
fn()
|
||||
return ChangeHat("", token)
|
||||
}
|
||||
|
||||
func ChangeProfile(subprofile string) error {
|
||||
var ret uintptr
|
||||
|
||||
if subprofile != "" {
|
||||
subProfileC := C.CString(subprofile)
|
||||
defer C.free(unsafe.Pointer(subProfileC))
|
||||
ret = uintptr(C.go_aa_change_profile(subProfileC))
|
||||
} else {
|
||||
ret = uintptr(C.go_aa_change_profile(nil))
|
||||
}
|
||||
|
||||
if ret != 0 {
|
||||
return syscall.Errno(ret)
|
||||
}
|
||||
return nil
|
||||
}
|
21
internal/apparmor/apparmor.c
Normal file
21
internal/apparmor/apparmor.c
Normal file
|
@ -0,0 +1,21 @@
|
|||
#include "./apparmor.h"
|
||||
|
||||
int go_aa_change_hat(const char *hat, unsigned long magic)
|
||||
{
|
||||
int ret = aa_change_hat(hat, magic);
|
||||
if (ret < 0)
|
||||
{
|
||||
return errno;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int go_aa_change_profile(const char *profile)
|
||||
{
|
||||
int ret = aa_change_profile(profile);
|
||||
if (ret < 0)
|
||||
{
|
||||
return errno;
|
||||
}
|
||||
return 0;
|
||||
}
|
8
internal/apparmor/apparmor.h
Normal file
8
internal/apparmor/apparmor.h
Normal file
|
@ -0,0 +1,8 @@
|
|||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <errno.h>
|
||||
#include <sys/apparmor.h>
|
||||
|
||||
int go_aa_change_hat(const char *hat, unsigned long magic);
|
||||
|
||||
int go_aa_change_profile(const char *profile);
|
17
internal/apparmor/magic.go
Normal file
17
internal/apparmor/magic.go
Normal file
|
@ -0,0 +1,17 @@
|
|||
package apparmor
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func GetMagicToken() (uint64, error) {
|
||||
var buf [64 / 8]byte
|
||||
if _, err := rand.Read(buf[:]); err != nil {
|
||||
return 0, fmt.Errorf("failed to generate magic token: %v", err)
|
||||
}
|
||||
return uint64(buf[0])<<56 | uint64(buf[1])<<48 |
|
||||
uint64(buf[2])<<40 | uint64(buf[3])<<32 |
|
||||
uint64(buf[4])<<24 | uint64(buf[5])<<16 |
|
||||
uint64(buf[6])<<8 | uint64(buf[7]), nil
|
||||
}
|
|
@ -1,7 +1,9 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
@ -9,6 +11,7 @@ import (
|
|||
|
||||
"github.com/alexedwards/argon2id"
|
||||
"github.com/eternal-flame-AD/yoake/config"
|
||||
"github.com/eternal-flame-AD/yoake/internal/echoerror"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
|
@ -16,6 +19,20 @@ import (
|
|||
|
||||
const AuthSessionName = "auth_session"
|
||||
|
||||
var dummyHash string
|
||||
|
||||
func init() {
|
||||
var dummyPassword [16]byte
|
||||
_, err := rand.Read(dummyPassword[:])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
dummyHash, err = argon2id.CreateHash(string(dummyPassword[:]), Argon2IdParams)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
type RequestAuth struct {
|
||||
Present bool
|
||||
Valid bool
|
||||
|
@ -118,6 +135,8 @@ func issueSession(c echo.Context, period time.Duration, roles []string) error {
|
|||
sess.Options = &sessions.Options{
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
Secure: config.Config().Listen.Ssl.Use,
|
||||
}
|
||||
if period == 0 {
|
||||
period = time.Duration(config.Config().Auth.ValidMinutes) * time.Minute
|
||||
|
@ -139,6 +158,8 @@ type LoginForm struct {
|
|||
OtpResponse string `json:"otp_response" form:"otp_response"`
|
||||
}
|
||||
|
||||
var errInvalidUserPass = echoerror.NewHttp(http.StatusUnauthorized, errors.New("invalid username or password"))
|
||||
|
||||
func Register(g *echo.Group) (err error) {
|
||||
g.GET("/auth.json", func(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, GetRequestAuth(c))
|
||||
|
@ -181,30 +202,31 @@ func Register(g *echo.Group) (err error) {
|
|||
return echo.NewHTTPError(http.StatusBadRequest, "password required")
|
||||
}
|
||||
if user, ok := config.Config().Auth.Users[form.Username]; ok {
|
||||
if len(user.PublicKeyId) > 0 {
|
||||
if verifiedOtpPubId == "" {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "otp required")
|
||||
}
|
||||
found := 0
|
||||
for _, pubId := range user.PublicKeyId {
|
||||
found += subtle.ConstantTimeCompare([]byte(pubId[:12]), []byte(verifiedOtpPubId))
|
||||
}
|
||||
if found == 0 {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "incorrect key used")
|
||||
}
|
||||
} else if verifiedOtpPubId != "" {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "otp not required but you provided one, this may be an configuration error")
|
||||
}
|
||||
|
||||
if match, _ := argon2id.ComparePasswordAndHash(form.Password, user.Password); match {
|
||||
if len(user.PublicKeyId) > 0 {
|
||||
if verifiedOtpPubId == "" {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "otp required")
|
||||
}
|
||||
found := 0
|
||||
for _, pubId := range user.PublicKeyId {
|
||||
found += subtle.ConstantTimeCompare([]byte(pubId[:12]), []byte(verifiedOtpPubId))
|
||||
}
|
||||
if found == 0 {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "incorrect key used")
|
||||
}
|
||||
} else if verifiedOtpPubId != "" {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "otp not required but you provided one, this may be an configuration error")
|
||||
}
|
||||
|
||||
issueSession(c, 0, user.Roles)
|
||||
c.JSON(http.StatusOK, map[string]interface{}{"message": "ok", "ok": true})
|
||||
return nil
|
||||
} else {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "incorrect password")
|
||||
return errInvalidUserPass
|
||||
}
|
||||
}
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "invalid username")
|
||||
argon2id.ComparePasswordAndHash(form.Password, dummyHash)
|
||||
return errInvalidUserPass
|
||||
}, loginRateLimiter)
|
||||
g.DELETE("/login", func(c echo.Context) error {
|
||||
return issueSession(c, -1, nil)
|
||||
|
|
|
@ -2,6 +2,7 @@ package utilapi
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/alexedwards/argon2id"
|
||||
|
@ -36,6 +37,12 @@ func Register(g *echo.Group) (err error) {
|
|||
Store: limiterStore,
|
||||
}))
|
||||
}
|
||||
g.GET("/tryopen", func(c echo.Context) error {
|
||||
if _, err := os.ReadFile(c.QueryParam("path")); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.String(200, c.QueryParam("path"))
|
||||
}, auth.RequireMiddleware(auth.RoleAdmin))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@ package server
|
|||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/eternal-flame-AD/yoake/config"
|
||||
"github.com/eternal-flame-AD/yoake/internal/apparmor"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
|
@ -16,7 +18,21 @@ var Server = echo.New()
|
|||
var hosts = map[string]*Host{}
|
||||
|
||||
func init() {
|
||||
hatChanged := false
|
||||
Server.Any("/*", func(c echo.Context) (err error) {
|
||||
if !hatChanged {
|
||||
appArmor := config.Config().Listen.AppArmor
|
||||
if appArmor.Serve != "" {
|
||||
if key, err := apparmor.GetMagicToken(); err != nil {
|
||||
return err
|
||||
} else {
|
||||
if err := apparmor.ChangeHat(appArmor.Serve, key); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
hatChanged = true
|
||||
}
|
||||
req := c.Request()
|
||||
res := c.Response()
|
||||
host := hosts[strings.ToLower(req.Host)]
|
||||
|
|
|
@ -6,9 +6,9 @@
|
|||
<div class="row p-2">
|
||||
<div class="col">
|
||||
<div class="card border">
|
||||
<div class="card-header">Quote</div>
|
||||
<div class="card-body">
|
||||
<blockquote class="blockquote text-center">
|
||||
<div class="card-header">Welcome</div>
|
||||
<div class="card-body text-center">
|
||||
<blockquote class="blockquote">
|
||||
<p>
|
||||
夜明け前が一番暗い
|
||||
</p>
|
||||
|
@ -17,6 +17,42 @@
|
|||
</p>
|
||||
|
||||
</blockquote>
|
||||
<hr>
|
||||
<div id="welcome">
|
||||
<p>
|
||||
Welcome to yoake.yumechi.jp, Yumechi's <a target="_blank"
|
||||
href="https://en.wikipedia.org/wiki/Personal_information_manager">
|
||||
<abbr title="Personal Information Manager" class="initialism">PIM</abbr></a>.
|
||||
</p>
|
||||
<p>
|
||||
Built with
|
||||
<abbr title="Echo HTTP Framework"><a target="_blank" rel="nofollow noreferer"
|
||||
href="https://echo.labstack.com/">Echo</a></abbr>,
|
||||
<abbr title="Bootstrap CSS Framework"><a target="_blank" rel="nofollow noreferer"
|
||||
href="https://getbootstrap.com/">Bootstrap</a></abbr>,
|
||||
<abbr title="jQuery JavaScript Library"><a href="https://jquery.com/" target="_blank"
|
||||
rel="nofollow noreferer">jQuery</a></abbr>,
|
||||
and
|
||||
<abbr title="Golang Standard Library html/template"><a target="_blank"
|
||||
rel="nofollow noreferer"
|
||||
href="https://pkg.go.dev/html/template">html/template</a></abbr>.
|
||||
Intended to be a parody of the
|
||||
<abbr title="Terumo BCT Trima Accel(R)"><a href="https://www.terumobct.com/trima"
|
||||
target="_blank" rel="nofollow noreferer">Trima Accel (R)</a></abbr> UI.
|
||||
</p>
|
||||
<p>
|
||||
{{ if not $auth.Valid }}
|
||||
This website welcomes guests as well!
|
||||
Click around and I hope you cound have some fun!
|
||||
{{ else }}
|
||||
{{ if eq (index $auth.Roles 0) "admin" }}
|
||||
Welcome Home!
|
||||
{{ else }}
|
||||
Hello, my friend! You know the drill!
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -96,9 +132,61 @@
|
|||
</p>
|
||||
<button class="btn btn-primary" onclick="signin()">Sign In</button>
|
||||
{{ end }}
|
||||
<table class="table text-center">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tab Status Icon</th>
|
||||
<th>Meaning</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<img src={{ trima_img "icon_t_vista_procedure_optimal.gif" "url" }} />
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
Tab Selected (Active)
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<img src={{ trima_img "icon_t_vista_procedure_valid.gif" "url" }} />
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
Tab Available
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<img src={{ trima_img "icon_t_vista_procedure_ineligible.gif" "url" }} />
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
Addn. Authorization Required
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<img src={{ trima_img "icon_t_vista_procedure_questionable.gif" "url" }} />
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
Unknown
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<img src={{ trima_img "icon_t_vista_procedure_invalid.gif" "url" }} />
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
Access Denied
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
Loading…
Reference in a new issue