├── .dockerignore
├── publish
└── python
│ ├── MANIFEST.in
│ ├── setup.cfg
│ ├── setup.py
│ └── README.rst
├── data
├── deployment.png
├── logo
│ └── charon.png
├── test-selfsigned.key
└── test-selfsigned.crt
├── cmd
├── charond
│ ├── doc.go
│ ├── service.go
│ ├── main.go
│ └── config.go
├── charong
│ └── main.go
└── charonctl
│ ├── service.go
│ └── config.go
├── scripts
├── docker-healthcheck.sh
├── generate.sh
└── docker-entrypoint.sh
├── .circleci
└── scripts
│ ├── get_tool.sh
│ ├── install_protoc.sh
│ ├── test.sh
│ └── generate.sh
├── internal
├── charonctl
│ ├── error.go
│ ├── console.go
│ ├── console_obtain_refresh_token.go
│ └── console_register_user.go
├── session
│ ├── actor.go
│ ├── sessionmock
│ │ └── actor_provider.go
│ ├── session.go
│ ├── session_test.go
│ └── actor_provider.go
├── mapping
│ ├── common.go
│ ├── group.go
│ ├── user.go
│ └── refresh_token.go
├── charond
│ ├── error.go
│ ├── helpers.go
│ ├── handler.go
│ ├── health.go
│ ├── health_test.go
│ ├── handler_logout.go
│ ├── suite_postgres_test.go
│ ├── helpers_test.go
│ ├── handler_register_permissions.go
│ ├── handler_get_permission.go
│ ├── handler_is_authenticated.go
│ ├── handler_belongs_to.go
│ ├── handler_is_granted.go
│ ├── handler_list_user_groups.go
│ ├── handler_list_user_permissions.go
│ ├── handler_logout_test.go
│ ├── handler_list_groups.go
│ ├── handler_get_group.go
│ ├── handler_set_user_groups.go
│ ├── handler_list_permissions.go
│ ├── suite_test.go
│ ├── handler_list_group_permissions.go
│ ├── handler_create_group.go
│ ├── database.go
│ ├── handler_delete_group.go
│ ├── handler_list_refresh_tokens.go
│ ├── handler_set_user_permissions.go
│ ├── handler_actor.go
│ ├── handler_set_group_permissions.go
│ ├── handler_modify_group.go
│ ├── daemon_test.go
│ ├── handler_get_user.go
│ ├── handler_login.go
│ ├── service.go
│ ├── handler_create_refresh_token.go
│ ├── handler_delete_user.go
│ ├── handler_revoke_refresh_token.go
│ ├── handler_list_users.go
│ └── handler_test.go
├── model
│ ├── suite_test.go
│ ├── group_permissions.go
│ ├── modelmock
│ │ ├── composition_writer.go
│ │ ├── group_permissions_provider.go
│ │ ├── user_permissions_provider.go
│ │ ├── permission_registry.go
│ │ ├── rows.go
│ │ └── user_groups_provider.go
│ ├── helpers.go
│ ├── user_permissions.go
│ ├── suite_postgres_test.go
│ ├── group_permissions_test.go
│ ├── user_permissions_test.go
│ ├── refresh_token.go
│ ├── group_test.go
│ ├── user_groups.go
│ └── permission_test.go
├── refreshtoken
│ └── refresh_token.go
├── service
│ ├── servicemock
│ │ └── user_finder.go
│ ├── logger
│ │ └── logger.go
│ └── user_finder.go
├── password
│ ├── passwordmock
│ │ └── hasher.go
│ ├── password.go
│ └── password_test.go
└── grpcerr
│ ├── example_test.go
│ └── grpc.go
├── pb
└── rpc
│ └── charond
│ ├── v1
│ ├── common.proto
│ ├── permission.proto
│ ├── auth.proto
│ ├── refresh_token.proto
│ ├── group.proto
│ └── common.pb.go
│ └── v1mock
│ ├── is_login_request__strategy.go
│ ├── permission_manager_server.go
│ ├── refresh_token_manager_server.go
│ ├── permission_manager_client.go
│ └── refresh_token_manager_client.go
├── .gitignore
├── .codeclimate.yml
├── Dockerfile
├── docker-compose.yml
├── LICENSE
├── Makefile
├── README.md
├── pkg
└── securitycontext
│ ├── security_context.go
│ └── security_context_test.go
├── example
└── client
│ └── main.go
├── Gopkg.toml
└── presentation.slide
/.dockerignore:
--------------------------------------------------------------------------------
1 | *
2 | !bin
3 | !scripts
--------------------------------------------------------------------------------
/publish/python/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include VERSION.txt
2 | include README.md
--------------------------------------------------------------------------------
/publish/python/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | description-file=README.txt
--------------------------------------------------------------------------------
/data/deployment.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piotrkowalczuk/charon/HEAD/data/deployment.png
--------------------------------------------------------------------------------
/data/logo/charon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piotrkowalczuk/charon/HEAD/data/logo/charon.png
--------------------------------------------------------------------------------
/cmd/charond/doc.go:
--------------------------------------------------------------------------------
1 | // Package main is server implementation of Charon auth service.
2 | package main
3 |
--------------------------------------------------------------------------------
/scripts/docker-healthcheck.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -e
3 |
4 | : ${CHAROND_PORT:=8080}
5 | curl -f http://localhost:$((CHAROND_PORT+1))/healthz || exit 1
--------------------------------------------------------------------------------
/.circleci/scripts/get_tool.sh:
--------------------------------------------------------------------------------
1 | rm -rf tmp/tools/$1
2 | mkdir -p tmp/tools/$1
3 | cd tmp/tools/$1
4 | GO111MODULE=on go mod init
5 | GO111MODULE=on go get $1@$2
6 | GO111MODULE=on go install $1
--------------------------------------------------------------------------------
/internal/charonctl/error.go:
--------------------------------------------------------------------------------
1 | package charonctl
2 |
3 | type Error struct {
4 | Msg string
5 | Err error
6 | }
7 |
8 | // Error implements error interface.
9 | func (e *Error) Error() string {
10 | return e.Msg
11 | }
12 |
--------------------------------------------------------------------------------
/internal/session/actor.go:
--------------------------------------------------------------------------------
1 | package session
2 |
3 | import (
4 | "github.com/piotrkowalczuk/charon"
5 | "github.com/piotrkowalczuk/charon/internal/model"
6 | "github.com/piotrkowalczuk/mnemosyne/mnemosynerpc"
7 | )
8 |
9 | type Actor struct {
10 | User *model.UserEntity
11 | Session *mnemosynerpc.Session
12 | Permissions charon.Permissions
13 | IsLocal bool
14 | }
15 |
--------------------------------------------------------------------------------
/pb/rpc/charond/v1/common.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package charon.rpc.charond.v1;
4 |
5 | option go_package = "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1;charond";
6 | option java_multiple_files = true;
7 | option java_package = "com.github.charon.rpc.charond.v1";
8 |
9 | // Order represents single field within OrderBy clause.
10 | message Order {
11 | string name = 1;
12 | bool descending = 2;
13 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vagrant
2 | .idea
3 | .glide
4 | tmp
5 | example/client/client
6 | vendor
7 | _vendor-*
8 | run.sh
9 | coverage.txt
10 | *.out
11 | bin
12 |
13 | publish/VERSION.txt
14 |
15 | *.xml
16 | *.test
17 | *.zip
18 |
19 | // Python
20 | venv
21 | charon.iml
22 | charon_client.egg-info
23 | *.pyc
24 | publish/python/github*
25 | publish/python/charon_client*
26 | publish/python/charon-client*
27 | publish/python/dist
28 |
29 | // Java
30 | publish/java
--------------------------------------------------------------------------------
/cmd/charond/service.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "net"
5 | "strconv"
6 |
7 | "go.uber.org/zap"
8 | )
9 |
10 | func initListener(logger *zap.Logger, host string, port int) net.Listener {
11 | on := host + ":" + strconv.FormatInt(int64(port), 10)
12 | listener, err := net.Listen("tcp", on)
13 | if err != nil {
14 | logger.Fatal("listener initialization failure", zap.Error(err), zap.String("host", host), zap.Int("port", port))
15 | }
16 | return listener
17 | }
18 |
--------------------------------------------------------------------------------
/internal/mapping/common.go:
--------------------------------------------------------------------------------
1 | package mapping
2 |
3 | import (
4 | charonrpc "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1"
5 | "github.com/piotrkowalczuk/charon/internal/model"
6 | )
7 |
8 | func OrderBy(in []*charonrpc.Order) []model.RowOrder {
9 | out := make([]model.RowOrder, 0, len(in))
10 | for _, i := range in {
11 | out = append(out, model.RowOrder{
12 | Name: i.GetName(),
13 | Descending: i.GetDescending(),
14 | })
15 | }
16 | return out
17 | }
18 |
--------------------------------------------------------------------------------
/internal/charond/error.go:
--------------------------------------------------------------------------------
1 | package charond
2 |
3 | import (
4 | "github.com/piotrkowalczuk/charon/internal/grpcerr"
5 | "google.golang.org/grpc/codes"
6 | "google.golang.org/grpc/status"
7 | )
8 |
9 | func handleMnemosyneError(err error) error {
10 | if sts, ok := status.FromError(err); ok && sts.Code() == codes.NotFound {
11 | return grpcerr.E(codes.Unauthenticated, "session not found")
12 | }
13 |
14 | return grpcerr.E(codes.Internal, "session fetch failure", err)
15 | }
16 |
--------------------------------------------------------------------------------
/.circleci/scripts/install_protoc.sh:
--------------------------------------------------------------------------------
1 | echo "protoc installation in ${PWD}/tmp/bin"
2 |
3 | curl -L https://github.com/google/protobuf/releases/download/v3.7.1/protoc-3.7.1-linux-x86_64.zip > protoc.zip
4 |
5 | rm -rf ./tmp/protoc ./tmp/pb/google
6 | mkdir -p ./tmp/protoc ./tmp/bin ./tmp/pb/google
7 |
8 | unzip protoc.zip -d ./tmp/protoc
9 |
10 | mv -f ./tmp/protoc/bin/protoc ./tmp/bin/protoc
11 | mv -f ./tmp/protoc/include/google ./tmp/pb
12 |
13 | rm -rf ./tmp/protoc
14 |
15 | ./tmp/bin/protoc --version
--------------------------------------------------------------------------------
/pb/rpc/charond/v1mock/is_login_request__strategy.go:
--------------------------------------------------------------------------------
1 | // Code generated by mockery v1.0.0. DO NOT EDIT.
2 |
3 | package charondmock
4 |
5 | import mock "github.com/stretchr/testify/mock"
6 |
7 | // isLoginRequest_Strategy is an autogenerated mock type for the isLoginRequest_Strategy type
8 | type isLoginRequest_Strategy struct {
9 | mock.Mock
10 | }
11 |
12 | // isLoginRequest_Strategy provides a mock function with given fields:
13 | func (_m *isLoginRequest_Strategy) isLoginRequest_Strategy() {
14 | _m.Called()
15 | }
16 |
--------------------------------------------------------------------------------
/.circleci/scripts/test.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | : ${TEST_RESULTS:=.}
4 |
5 | set -e
6 |
7 |
8 | gotestsum --junitfile results.xml -- -p=1 -count=1 -race -coverprofile=cover-source.out -covermode=atomic -v ./...
9 | cat cover-source.out | grep -v '.pb.go' > cover-step1.out
10 | cat cover-step1.out | grep -v 'mock' > cover-step2.out
11 | cat cover-step2.out | grep -v '/pb/' > cover-step3.out
12 | cat cover-step3.out | grep -v '/example/' > cover-step4.out
13 | cat cover-step4.out | grep -v '.pqt.go' > cover.out
14 |
15 |
--------------------------------------------------------------------------------
/internal/model/suite_test.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "flag"
5 | "os"
6 | "testing"
7 |
8 | _ "github.com/lib/pq"
9 | )
10 |
11 | var (
12 | testPostgresAddress string
13 | )
14 |
15 | func TestMain(m *testing.M) {
16 | flag.StringVar(&testPostgresAddress, "postgres.address", getStringEnvOr("CHAROND_POSTGRES_ADDRESS", "postgres://localhost/test?sslmode=disable"), "")
17 | flag.Parse()
18 |
19 | os.Exit(m.Run())
20 | }
21 |
22 | func getStringEnvOr(env, or string) string {
23 | if v := os.Getenv(env); v != "" {
24 | return v
25 | }
26 | return or
27 | }
28 |
--------------------------------------------------------------------------------
/.codeclimate.yml:
--------------------------------------------------------------------------------
1 | version: "2"
2 | plugins:
3 | golint:
4 | enabled: true
5 | gofmt:
6 | enabled: true
7 | govet:
8 | enabled: true
9 | checks:
10 | complex-logic:
11 | enabled: false
12 | method-complexity:
13 | enabled: false
14 | file-lines:
15 | enabled: false
16 | method-lines:
17 | enabled: false
18 | return-statements:
19 | enabled: false
20 | exclude_patterns:
21 | - "**/*_test.go"
22 | - "**/*_pb.py"
23 | - "**/*_pb2.py"
24 | - "**/*.pb.go"
25 | - "**/*.pqt.go"
26 | - "**/mock_*.go"
27 | - "pb/"
28 | - "scripts/"
29 | - "vendor/**/*"
--------------------------------------------------------------------------------
/scripts/generate.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | SERVICE=charon
4 |
5 | cd ./internal/model && charong && cd -
6 | goimports -w ./internal/model
7 | mockery -case=underscore -dir=./internal/model -all -output=./internal/model/modelmock -outpkg=modelmock
8 | mockery -case=underscore -dir=./internal/session -all -output=./internal/session/sessionmock -outpkg=sessionmock
9 | mockery -case=underscore -dir=./internal/password -all -output=./internal/password/passwordmock -outpkg=passwordmock
10 | mockery -case=underscore -dir=./internal/service -all -output=./internal/service/servicemock -outpkg=servicemock
11 | mockery -case=underscore -dir=./${SERVICE}rpc -all -output=./${SERVICE}test -outpkg=${SERVICE}test
12 | goimports -w ./internal/model
13 |
14 |
15 |
--------------------------------------------------------------------------------
/internal/charond/helpers.go:
--------------------------------------------------------------------------------
1 | package charond
2 |
3 | import (
4 | "github.com/golang/protobuf/ptypes/empty"
5 | "github.com/piotrkowalczuk/ntypes"
6 | )
7 |
8 | func untouched(given, created, removed int64) int64 {
9 | switch {
10 | case given < 0:
11 | return -1
12 | case given == 0:
13 | return -2
14 | case given < created:
15 | return 0
16 | default:
17 | return given - created
18 | }
19 | }
20 |
21 | func none() *empty.Empty {
22 | return &empty.Empty{}
23 | }
24 |
25 | func allocNilString(s *ntypes.String) ntypes.String {
26 | if s == nil {
27 | return ntypes.String{}
28 | }
29 | return *s
30 | }
31 |
32 | func allocNilBool(b *ntypes.Bool) ntypes.Bool {
33 | if b == nil {
34 | return ntypes.Bool{}
35 | }
36 | return *b
37 | }
38 |
--------------------------------------------------------------------------------
/internal/model/group_permissions.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "context"
5 | "database/sql"
6 | )
7 |
8 | // GroupPermissionsProvider ...
9 | type GroupPermissionsProvider interface {
10 | Insert(context.Context, *GroupPermissionsEntity) (*GroupPermissionsEntity, error)
11 | }
12 |
13 | // GroupPermissionsRepository extends GroupPermissionsRepositoryBase
14 | type GroupPermissionsRepository struct {
15 | GroupPermissionsRepositoryBase
16 | }
17 |
18 | // NewGroupPermissionsRepository ...
19 | func NewGroupPermissionsRepository(dbPool *sql.DB) GroupPermissionsProvider {
20 | return &GroupPermissionsRepository{
21 | GroupPermissionsRepositoryBase{
22 | DB: dbPool,
23 | Table: TableGroupPermissions,
24 | Columns: TableGroupPermissionsColumns,
25 | },
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/internal/charond/handler.go:
--------------------------------------------------------------------------------
1 | package charond
2 |
3 | import (
4 | "github.com/piotrkowalczuk/charon/internal/session"
5 | "github.com/piotrkowalczuk/mnemosyne/mnemosynerpc"
6 | "go.uber.org/zap"
7 | )
8 |
9 | type handler struct {
10 | session.ActorProvider
11 |
12 | opts DaemonOpts
13 | logger *zap.Logger
14 | repository repositories
15 | session mnemosynerpc.SessionManagerClient
16 | }
17 |
18 | func newHandler(rs *rpcServer) *handler {
19 | h := &handler{
20 | opts: rs.opts,
21 | session: rs.session,
22 | repository: rs.repository,
23 | logger: rs.logger,
24 | ActorProvider: &session.MnemosyneActorProvider{
25 | Client: rs.session,
26 | UserProvider: rs.repository.user,
27 | PermissionProvider: rs.repository.permission,
28 | },
29 | }
30 |
31 | return h
32 | }
33 |
--------------------------------------------------------------------------------
/internal/charond/health.go:
--------------------------------------------------------------------------------
1 | package charond
2 |
3 | import (
4 | "context"
5 | "database/sql"
6 | "net/http"
7 | "time"
8 |
9 | "go.uber.org/zap"
10 | )
11 |
12 | type healthHandler struct {
13 | logger *zap.Logger
14 | postgres *sql.DB
15 | }
16 |
17 | func (hh *healthHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
18 | if hh.postgres != nil {
19 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
20 | defer cancel()
21 |
22 | if err := hh.postgres.PingContext(ctx); err != nil {
23 | hh.logger.Debug("health check failure due to postgres connection")
24 | http.Error(rw, "postgres ping failure", http.StatusServiceUnavailable)
25 | return
26 | }
27 | }
28 |
29 | hh.logger.Debug("successful health check")
30 | rw.WriteHeader(http.StatusOK)
31 | rw.Write([]byte("1"))
32 | }
33 |
--------------------------------------------------------------------------------
/internal/charond/health_test.go:
--------------------------------------------------------------------------------
1 | package charond
2 |
3 | import (
4 | "net/http"
5 | "net/http/httptest"
6 | "testing"
7 |
8 | "go.uber.org/zap"
9 | )
10 |
11 | func TestHealthHandler_ServeHTTP(t *testing.T) {
12 | l := zap.L()
13 | s := postgresSuite{
14 | logger: l,
15 | }
16 | s.setup(t)
17 |
18 | h := healthHandler{
19 | postgres: s.db,
20 | logger: l,
21 | }
22 |
23 | rw := httptest.NewRecorder()
24 | r := &http.Request{}
25 | h.ServeHTTP(rw, r)
26 | if rw.Code != http.StatusOK {
27 | t.Errorf("wrong status code, expected %d but got %d", http.StatusOK, rw.Code)
28 | }
29 |
30 | s.teardown(t)
31 |
32 | rw = httptest.NewRecorder()
33 | h.ServeHTTP(rw, r)
34 | if rw.Code != http.StatusServiceUnavailable {
35 | t.Errorf("wrong status code, expected %d but got %d", http.StatusServiceUnavailable, rw.Code)
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/internal/model/modelmock/composition_writer.go:
--------------------------------------------------------------------------------
1 | // Code generated by mockery v1.0.0. DO NOT EDIT.
2 |
3 | package modelmock
4 |
5 | import mock "github.com/stretchr/testify/mock"
6 | import model "github.com/piotrkowalczuk/charon/internal/model"
7 |
8 | // CompositionWriter is an autogenerated mock type for the CompositionWriter type
9 | type CompositionWriter struct {
10 | mock.Mock
11 | }
12 |
13 | // WriteComposition provides a mock function with given fields: _a0, _a1, _a2
14 | func (_m *CompositionWriter) WriteComposition(_a0 string, _a1 *model.Composer, _a2 *model.CompositionOpts) error {
15 | ret := _m.Called(_a0, _a1, _a2)
16 |
17 | var r0 error
18 | if rf, ok := ret.Get(0).(func(string, *model.Composer, *model.CompositionOpts) error); ok {
19 | r0 = rf(_a0, _a1, _a2)
20 | } else {
21 | r0 = ret.Error(0)
22 | }
23 |
24 | return r0
25 | }
26 |
--------------------------------------------------------------------------------
/internal/charond/handler_logout.go:
--------------------------------------------------------------------------------
1 | package charond
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/golang/protobuf/ptypes/empty"
7 | charonrpc "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1"
8 | "github.com/piotrkowalczuk/charon/internal/grpcerr"
9 | "github.com/piotrkowalczuk/mnemosyne/mnemosynerpc"
10 |
11 | "google.golang.org/grpc/codes"
12 | )
13 |
14 | type logoutHandler struct {
15 | *handler
16 | }
17 |
18 | func (lh *logoutHandler) Logout(ctx context.Context, r *charonrpc.LogoutRequest) (*empty.Empty, error) {
19 | if len(r.AccessToken) == 0 {
20 | return nil, grpcerr.E(codes.InvalidArgument, "empty session id, logout aborted")
21 | }
22 |
23 | _, err := lh.session.Abandon(ctx, &mnemosynerpc.AbandonRequest{
24 | AccessToken: r.AccessToken,
25 | })
26 | if err != nil {
27 | return nil, err
28 | }
29 |
30 | return &empty.Empty{}, nil
31 | }
32 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine:latest
2 | MAINTAINER Piotr Kowalczuk
3 |
4 | ARG BUILD_DATE
5 | ARG VCS_REF
6 |
7 | LABEL org.label-schema.build-date=$BUILD_DATE \
8 | org.label-schema.docker.dockerfile="Dockerfile" \
9 | org.label-schema.license="ASL" \
10 | org.label-schema.name="charon" \
11 | org.label-schema.url="https://github.com/piotrkowalczuk/charon" \
12 | org.label-schema.vcs-ref=$VCS_REF \
13 | org.label-schema.vcs-type="git" \
14 | org.label-schema.vcs-url="https://github.com/piotrkowalczuk/charon"
15 |
16 | COPY ./bin /usr/local/bin/
17 | COPY ./scripts/docker-entrypoint.sh /
18 | COPY ./scripts/docker-healthcheck.sh /
19 |
20 | RUN apk --no-cache add curl
21 |
22 | VOLUME /data
23 | EXPOSE 8080 8081
24 |
25 | HEALTHCHECK --interval=1m30s --timeout=3s CMD ["/docker-healthcheck.sh"]
26 | ENTRYPOINT ["/docker-entrypoint.sh"]
27 | CMD ["charond"]
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '2'
2 |
3 | services:
4 | postgres:
5 | image: postgres:9.5
6 | container_name: postgres
7 | ports:
8 | - 5434:5432
9 | volumes:
10 | - postgres-data:/var/lib/postgresql/data
11 | mnemosyned:
12 | image: piotrkowalczuk/mnemosyne:v0.8.0
13 | container_name: mnemosyned
14 | environment:
15 | MNEMOSYNED_POSTGRES_SCHEMA: mnemosyne
16 | links:
17 | - postgres
18 | ports:
19 | - 10000:8080
20 | - 10001:8081
21 | charond:
22 | image: charon
23 | container_name: charond
24 | environment:
25 | MNEMOSYNED_POSTGRES_SCHEMA: mnemosyne
26 | links:
27 | - postgres
28 | ports:
29 | - 10010:8080
30 | - 10011:8081
31 | depends_on:
32 | - mnemosyned
33 | volumes:
34 | postgres-data:
35 | external: false
36 | prometheus-data:
37 | external: false
--------------------------------------------------------------------------------
/internal/refreshtoken/refresh_token.go:
--------------------------------------------------------------------------------
1 | package refreshtoken
2 |
3 | import (
4 | "crypto/rand"
5 | "encoding/hex"
6 | "io"
7 |
8 | "golang.org/x/crypto/sha3"
9 | )
10 |
11 | // Random generate refresh token with given key and generated hash of length 16.
12 | func Random() (string, error) {
13 | buf, err := generateRandomBytes(64)
14 | if err != nil {
15 | return "", err
16 | }
17 |
18 | // A hash needs to be 64 bytes long to have 256-bit collision resistance.
19 | hash := make([]byte, 32)
20 | // Compute a 64-byte hash of buf and put it in h.
21 | sha3.ShakeSum128(hash, buf)
22 | hash2 := make([]byte, hex.EncodedLen(len(hash)))
23 | hex.Encode(hash2, hash)
24 | return string(hash2), nil
25 | }
26 |
27 | func generateRandomBytes(length int) ([]byte, error) {
28 | k := make([]byte, length)
29 | if _, err := io.ReadFull(rand.Reader, k); err != nil {
30 | return nil, err
31 | }
32 | return k, nil
33 | }
34 |
--------------------------------------------------------------------------------
/internal/model/helpers.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "database/sql"
5 |
6 | "go.uber.org/zap"
7 | )
8 |
9 | func untouched(given, created, removed int64) int64 {
10 | switch {
11 | case given < 0:
12 | return -1
13 | case given == 0:
14 | return -2
15 | case given < created:
16 | return 0
17 | default:
18 | return given - created
19 | }
20 | }
21 |
22 | func initPostgres(address string, test bool, logger *zap.Logger) (*sql.DB, error) {
23 | postgres, err := sql.Open("postgres", address)
24 | if err != nil {
25 | return nil, err
26 | }
27 |
28 | if test {
29 | if err = teardownDatabase(postgres); err != nil {
30 | return nil, err
31 | }
32 | logger.Info("database has been cleared upfront")
33 | }
34 | err = setupDatabase(postgres)
35 | if err != nil {
36 | return nil, err
37 | }
38 |
39 | logger.Info("postgres connection has been established", zap.String("address", address))
40 |
41 | return postgres, nil
42 | }
43 |
--------------------------------------------------------------------------------
/publish/python/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 | from subprocess import check_output
3 |
4 | with open('../VERSION.txt', 'r') as content_file:
5 | version = content_file.read()
6 |
7 | setup(
8 | name='charon-client',
9 | version=version[1:],
10 | description='charon service grpc client library',
11 | url='http://github.com/piotrkowalczuk/charon',
12 | author='Piotr Kowalczuk',
13 | author_email='p.kowalczuk.priv@gmail.com',
14 | license='MIT',
15 | packages=['github.com.piotrkowalczuk.charon.pb.rpc.charond.v1'],
16 | install_requires=[
17 | 'protobuf',
18 | 'grpcio',
19 | 'protobuf-ntypes',
20 | 'protobuf-qtypes',
21 | ],
22 | zip_safe=False,
23 | keywords=['charon', 'grpc', 'authentication', 'authorization', 'service', 'client'],
24 | download_url='https://github.com/piotrkowalczuk/charon/archive/%s.tar.gz' % version.rstrip(),
25 | )
--------------------------------------------------------------------------------
/internal/service/servicemock/user_finder.go:
--------------------------------------------------------------------------------
1 | // Code generated by mockery v1.0.0. DO NOT EDIT.
2 |
3 | package servicemock
4 |
5 | import context "context"
6 | import mock "github.com/stretchr/testify/mock"
7 | import model "github.com/piotrkowalczuk/charon/internal/model"
8 |
9 | // UserFinder is an autogenerated mock type for the UserFinder type
10 | type UserFinder struct {
11 | mock.Mock
12 | }
13 |
14 | // FindUser provides a mock function with given fields: _a0
15 | func (_m *UserFinder) FindUser(_a0 context.Context) (*model.UserEntity, error) {
16 | ret := _m.Called(_a0)
17 |
18 | var r0 *model.UserEntity
19 | if rf, ok := ret.Get(0).(func(context.Context) *model.UserEntity); ok {
20 | r0 = rf(_a0)
21 | } else {
22 | if ret.Get(0) != nil {
23 | r0 = ret.Get(0).(*model.UserEntity)
24 | }
25 | }
26 |
27 | var r1 error
28 | if rf, ok := ret.Get(1).(func(context.Context) error); ok {
29 | r1 = rf(_a0)
30 | } else {
31 | r1 = ret.Error(1)
32 | }
33 |
34 | return r0, r1
35 | }
36 |
--------------------------------------------------------------------------------
/internal/session/sessionmock/actor_provider.go:
--------------------------------------------------------------------------------
1 | // Code generated by mockery v1.0.0. DO NOT EDIT.
2 |
3 | package sessionmock
4 |
5 | import context "context"
6 | import mock "github.com/stretchr/testify/mock"
7 | import session "github.com/piotrkowalczuk/charon/internal/session"
8 |
9 | // ActorProvider is an autogenerated mock type for the ActorProvider type
10 | type ActorProvider struct {
11 | mock.Mock
12 | }
13 |
14 | // Actor provides a mock function with given fields: _a0
15 | func (_m *ActorProvider) Actor(_a0 context.Context) (*session.Actor, error) {
16 | ret := _m.Called(_a0)
17 |
18 | var r0 *session.Actor
19 | if rf, ok := ret.Get(0).(func(context.Context) *session.Actor); ok {
20 | r0 = rf(_a0)
21 | } else {
22 | if ret.Get(0) != nil {
23 | r0 = ret.Get(0).(*session.Actor)
24 | }
25 | }
26 |
27 | var r1 error
28 | if rf, ok := ret.Get(1).(func(context.Context) error); ok {
29 | r1 = rf(_a0)
30 | } else {
31 | r1 = ret.Error(1)
32 | }
33 |
34 | return r0, r1
35 | }
36 |
--------------------------------------------------------------------------------
/publish/python/README.rst:
--------------------------------------------------------------------------------
1 | #################
2 | Charon Client
3 | #################
4 |
5 | TODO
6 |
7 | *************
8 | Example
9 | *************
10 |
11 | ::
12 |
13 | $ pip install charon-client
14 |
15 |
16 | .. code-block:: python
17 |
18 | from github.com.piotrkowalczuk.charon.pb.rpc.charond.v1 import auth_pb2, auth_pb2_grpc
19 | import grpc
20 |
21 | charonChannel = grpc.insecure_channel('ADDRESS')
22 | auth = auth_pb2_grpc.AuthStub(charonChannel)
23 | try:
24 | res = auth.Login(auth_pb2.LoginRequest(
25 | username="USERNAME",
26 | password="PASSWORD",
27 | ))
28 |
29 | print "access token: %s" % res.value
30 | except grpc.RpcError as e:
31 | if e.code() == grpc.StatusCode.UNAUTHENTICATED:
32 | print "login failure, username and/or password do not match"
33 | else:
34 | print "grpc error: %s" % e
35 | except Exception as e:
36 | print "unexpected error: %s" % e
37 |
38 |
39 |
--------------------------------------------------------------------------------
/internal/session/session.go:
--------------------------------------------------------------------------------
1 | package session
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "strconv"
7 | )
8 |
9 | const (
10 | actorIDPrefix = "charon:user:"
11 | )
12 |
13 | // ActorID is globally unique identifier that in format "charon:user:".
14 | type ActorID string
15 |
16 | // ActorIDFromInt64 allocate ActorID using given user id.
17 | func ActorIDFromInt64(userID int64) ActorID {
18 | return ActorID(actorIDPrefix + strconv.FormatInt(userID, 10))
19 | }
20 |
21 | // String implements fmt.Stringer interface.
22 | func (ai ActorID) String() string {
23 | return string(ai)
24 | }
25 |
26 | // UserID returns user id if possible, otherwise an error.
27 | func (ai ActorID) UserID() (int64, error) {
28 | if len(ai) < 13 {
29 | return 0, errors.New("charon: session actor id to short, min length 13 characters")
30 | }
31 | if ai[:12] != actorIDPrefix {
32 | return 0, fmt.Errorf("charon: session actor id wrong prefix expected %s, got %s", actorIDPrefix, ai[:12])
33 | }
34 |
35 | return strconv.ParseInt(string(ai)[12:], 10, 64)
36 | }
37 |
--------------------------------------------------------------------------------
/internal/charond/suite_postgres_test.go:
--------------------------------------------------------------------------------
1 | package charond
2 |
3 | import (
4 | "database/sql"
5 | "testing"
6 |
7 | "go.uber.org/zap"
8 | )
9 |
10 | type postgresSuite struct {
11 | logger *zap.Logger
12 | db *sql.DB
13 | repository repositories
14 | }
15 |
16 | func (ps *postgresSuite) setup(t *testing.T) {
17 | if testing.Short() {
18 | t.Skip("postgres suite ignored in short mode")
19 | }
20 |
21 | var err error
22 |
23 | ps.logger = zap.L()
24 | ps.db, err = initPostgres(testPostgresAddress, true, ps.logger)
25 | if err != nil {
26 | t.Fatalf("postgres connection (%s) error: %s", testPostgresAddress, err.Error())
27 | }
28 |
29 | ps.repository = newRepositories(ps.db)
30 | }
31 |
32 | func (ps *postgresSuite) teardown(t *testing.T) {
33 | var err error
34 |
35 | if err = teardownDatabase(ps.db); err != nil {
36 | t.Fatalf("postgres suite database teardown error: %s", err.Error())
37 | }
38 | if err = ps.db.Close(); err != nil {
39 | t.Fatalf("postgres suite teardown database connection error: %s", err.Error())
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Piotr Kowalczuk
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/internal/charond/helpers_test.go:
--------------------------------------------------------------------------------
1 | package charond
2 |
3 | import "testing"
4 |
5 | func TestUntouched(t *testing.T) {
6 | data := []struct {
7 | given, created, removed, untouched int64
8 | }{
9 | {
10 | given: -1000,
11 | created: 2,
12 | removed: 10,
13 | untouched: -1,
14 | },
15 | {
16 | given: 0,
17 | created: 0,
18 | removed: 0,
19 | untouched: -2,
20 | },
21 | {
22 | given: 5,
23 | created: 3,
24 | removed: 0,
25 | untouched: 2,
26 | },
27 | {
28 | given: 10,
29 | created: 0,
30 | removed: 0,
31 | untouched: 10,
32 | },
33 | {
34 | given: 100,
35 | created: 100,
36 | removed: 100,
37 | untouched: 0,
38 | },
39 | {
40 | given: 5,
41 | created: 0,
42 | removed: 100,
43 | untouched: 5,
44 | },
45 | {
46 | given: 5,
47 | created: 6,
48 | removed: 0,
49 | untouched: 0,
50 | },
51 | }
52 |
53 | for _, d := range data {
54 | u := untouched(d.given, d.created, d.removed)
55 | if u != d.untouched {
56 | t.Errorf("wrong value, expected %d but got %d", d.untouched, u)
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/internal/password/passwordmock/hasher.go:
--------------------------------------------------------------------------------
1 | // Code generated by mockery v1.0.0. DO NOT EDIT.
2 |
3 | package passwordmock
4 |
5 | import mock "github.com/stretchr/testify/mock"
6 |
7 | // Hasher is an autogenerated mock type for the Hasher type
8 | type Hasher struct {
9 | mock.Mock
10 | }
11 |
12 | // Compare provides a mock function with given fields: _a0, _a1
13 | func (_m *Hasher) Compare(_a0 []byte, _a1 []byte) bool {
14 | ret := _m.Called(_a0, _a1)
15 |
16 | var r0 bool
17 | if rf, ok := ret.Get(0).(func([]byte, []byte) bool); ok {
18 | r0 = rf(_a0, _a1)
19 | } else {
20 | r0 = ret.Get(0).(bool)
21 | }
22 |
23 | return r0
24 | }
25 |
26 | // Hash provides a mock function with given fields: _a0
27 | func (_m *Hasher) Hash(_a0 []byte) ([]byte, error) {
28 | ret := _m.Called(_a0)
29 |
30 | var r0 []byte
31 | if rf, ok := ret.Get(0).(func([]byte) []byte); ok {
32 | r0 = rf(_a0)
33 | } else {
34 | if ret.Get(0) != nil {
35 | r0 = ret.Get(0).([]byte)
36 | }
37 | }
38 |
39 | var r1 error
40 | if rf, ok := ret.Get(1).(func([]byte) error); ok {
41 | r1 = rf(_a0)
42 | } else {
43 | r1 = ret.Error(1)
44 | }
45 |
46 | return r0, r1
47 | }
48 |
--------------------------------------------------------------------------------
/internal/model/modelmock/group_permissions_provider.go:
--------------------------------------------------------------------------------
1 | // Code generated by mockery v1.0.0. DO NOT EDIT.
2 |
3 | package modelmock
4 |
5 | import context "context"
6 | import mock "github.com/stretchr/testify/mock"
7 | import model "github.com/piotrkowalczuk/charon/internal/model"
8 |
9 | // GroupPermissionsProvider is an autogenerated mock type for the GroupPermissionsProvider type
10 | type GroupPermissionsProvider struct {
11 | mock.Mock
12 | }
13 |
14 | // Insert provides a mock function with given fields: _a0, _a1
15 | func (_m *GroupPermissionsProvider) Insert(_a0 context.Context, _a1 *model.GroupPermissionsEntity) (*model.GroupPermissionsEntity, error) {
16 | ret := _m.Called(_a0, _a1)
17 |
18 | var r0 *model.GroupPermissionsEntity
19 | if rf, ok := ret.Get(0).(func(context.Context, *model.GroupPermissionsEntity) *model.GroupPermissionsEntity); ok {
20 | r0 = rf(_a0, _a1)
21 | } else {
22 | if ret.Get(0) != nil {
23 | r0 = ret.Get(0).(*model.GroupPermissionsEntity)
24 | }
25 | }
26 |
27 | var r1 error
28 | if rf, ok := ret.Get(1).(func(context.Context, *model.GroupPermissionsEntity) error); ok {
29 | r1 = rf(_a0, _a1)
30 | } else {
31 | r1 = ret.Error(1)
32 | }
33 |
34 | return r0, r1
35 | }
36 |
--------------------------------------------------------------------------------
/internal/session/session_test.go:
--------------------------------------------------------------------------------
1 | package session_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/piotrkowalczuk/charon/internal/session"
7 | )
8 |
9 | func TestUserIDFromSessionActorID(t *testing.T) {
10 | success := map[string]int64{
11 | "charon:user:1": 1,
12 | "charon:user:0": 0,
13 | "charon:user:12312412512512": 12312412512512,
14 | }
15 |
16 | for given, expected := range success {
17 | userID, err := session.ActorID(given).UserID()
18 | if err != nil {
19 | t.Errorf("unexpected error: %s", err.Error())
20 | } else if userID != expected {
21 | t.Errorf("wrong user id retrieved from session subject id expected %d, got %d", expected, userID)
22 | }
23 | }
24 |
25 | failures := []string{
26 | "",
27 | "charon",
28 | "charon:",
29 | "charon:user",
30 | "charon:user:",
31 | ":user:1",
32 | "user:1",
33 | ":1",
34 | "1",
35 | "1231251251241241241251251",
36 | "charon:resu:52151235125123",
37 | "charon:u:52151235125123",
38 | "charon:user:1234567890x",
39 | }
40 |
41 | for _, given := range failures {
42 | _, err := session.ActorID(given).UserID()
43 | if err == nil {
44 | t.Errorf("expected error %s", err.Error())
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/internal/charond/handler_register_permissions.go:
--------------------------------------------------------------------------------
1 | package charond
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/piotrkowalczuk/charon"
7 | charonrpc "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1"
8 | "github.com/piotrkowalczuk/charon/internal/grpcerr"
9 | "github.com/piotrkowalczuk/charon/internal/model"
10 | "google.golang.org/grpc/codes"
11 | )
12 |
13 | type registerPermissionsHandler struct {
14 | *handler
15 | registry model.PermissionRegistry
16 | }
17 |
18 | func (rph *registerPermissionsHandler) Register(ctx context.Context, req *charonrpc.RegisterPermissionsRequest) (*charonrpc.RegisterPermissionsResponse, error) {
19 | permissions := charon.NewPermissions(req.Permissions...)
20 | created, untouched, removed, err := rph.registry.Register(ctx, permissions)
21 | if err != nil {
22 | switch err {
23 | case model.ErrEmptySliceOfPermissions, model.ErrEmptySubsystem, model.ErrorInconsistentSubsystem:
24 | return nil, grpcerr.E(codes.InvalidArgument, err)
25 | default:
26 | return nil, grpcerr.E(codes.Internal, "permission registration failure", err)
27 | }
28 | }
29 |
30 | return &charonrpc.RegisterPermissionsResponse{
31 | Created: created,
32 | Untouched: untouched,
33 | Removed: removed,
34 | }, nil
35 | }
36 |
--------------------------------------------------------------------------------
/cmd/charong/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "io"
7 | "log"
8 | "os"
9 |
10 | "github.com/piotrkowalczuk/pqt/pqtgo/pqtgogen"
11 |
12 | "github.com/piotrkowalczuk/ntypespqt"
13 | "github.com/piotrkowalczuk/pqt/pqtsql"
14 | "github.com/piotrkowalczuk/qtypespqt"
15 | )
16 |
17 | var (
18 | schema, output string
19 | )
20 |
21 | func init() {
22 | flag.StringVar(&schema, "schema", "charon", "")
23 | flag.StringVar(&output, "output", "schema.pqt", "")
24 | }
25 |
26 | func main() {
27 | file, err := openFile(output)
28 | if err != nil {
29 | log.Fatal(err)
30 | }
31 | defer file.Close()
32 |
33 | sch := databaseSchema()
34 | genGo := &pqtgogen.Generator{
35 | Pkg: "model",
36 | Version: 9.5,
37 | Components: pqtgogen.ComponentAll,
38 | Plugins: []pqtgogen.Plugin{
39 | &qtypespqt.Plugin{},
40 | &ntypespqt.Plugin{},
41 | },
42 | }
43 | genSQL := &pqtsql.Generator{}
44 | if err := genGo.GenerateTo(sch, file); err != nil {
45 | log.Fatal(err)
46 | }
47 | fmt.Fprint(file, "const SQL = `\n")
48 | if err := genSQL.GenerateTo(sch, file); err != nil {
49 | log.Fatal(err)
50 | }
51 | fmt.Fprint(file, "`")
52 | }
53 |
54 | func openFile(output string) (io.WriteCloser, error) {
55 | return os.OpenFile(output+".go", os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0660)
56 | }
57 |
--------------------------------------------------------------------------------
/internal/grpcerr/example_test.go:
--------------------------------------------------------------------------------
1 | package grpcerr_test
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/piotrkowalczuk/charon/internal/grpcerr"
7 | )
8 |
9 | func ExampleError() {
10 | // Single error.
11 | e1 := grpcerr.E(grpcerr.Op("Get"), grpcerr.Kind("io"), "network unreachable")
12 | fmt.Println("\nSimple error:")
13 | fmt.Println(e1)
14 | // Nested error.
15 | fmt.Println("\nNested error:")
16 | e2 := grpcerr.E(grpcerr.Op("Read"), grpcerr.Kind("other"), e1)
17 | fmt.Println(e2)
18 | // Output:
19 | //
20 | // Simple error:
21 | // Get: io: network unreachable
22 | //
23 | // Nested error:
24 | // Read: io:
25 | // Get: network unreachable
26 | }
27 | func ExampleMatch() {
28 | err := grpcerr.E("network unreachable")
29 | // Construct an error, one we pretend to have received from a test.
30 | got := grpcerr.E(grpcerr.Op("Get"), grpcerr.Kind("io"), err)
31 | // Now construct a reference error, which might not have all
32 | // the fields of the error from the test.
33 | expect := grpcerr.E(grpcerr.Kind("io"), err)
34 | fmt.Println("Match:", grpcerr.Match(expect, got))
35 | // Now one that's incorrect - wrong Kind.
36 | got = grpcerr.E(grpcerr.Op("Get"), grpcerr.Kind("permission"), err)
37 | fmt.Println("Mismatch:", grpcerr.Match(expect, got))
38 | // Output:
39 | //
40 | // Match: true
41 | // Mismatch: false
42 | }
43 |
--------------------------------------------------------------------------------
/internal/model/user_permissions.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "context"
5 | "database/sql"
6 | "fmt"
7 | )
8 |
9 | // UserPermissionsProvider ...
10 | type UserPermissionsProvider interface {
11 | Insert(context.Context, *UserPermissionsEntity) (*UserPermissionsEntity, error)
12 | DeleteByUserID(context.Context, int64) (int64, error)
13 | }
14 |
15 | // UserPermissionsRepository extends UserPermissionsRepositoryBase
16 | type UserPermissionsRepository struct {
17 | UserPermissionsRepositoryBase
18 | deleteByUserIDQuery string
19 | }
20 |
21 | // NewUserPermissionsRepository ...
22 | func NewUserPermissionsRepository(dbPool *sql.DB) UserPermissionsProvider {
23 | return &UserPermissionsRepository{
24 | UserPermissionsRepositoryBase: UserPermissionsRepositoryBase{
25 | DB: dbPool,
26 | Table: TableUserPermissions,
27 | Columns: TableUserPermissionsColumns,
28 | },
29 | deleteByUserIDQuery: fmt.Sprintf("DELETE FROM %s WHERE %s = $1", TableUserPermissions, TableUserPermissionsColumnUserID),
30 | }
31 | }
32 |
33 | // DeleteByUserID removes all permissions of given user.
34 | func (upr *UserPermissionsRepository) DeleteByUserID(ctx context.Context, id int64) (int64, error) {
35 | res, err := upr.DB.ExecContext(ctx, upr.deleteByUserIDQuery, id)
36 | if err != nil {
37 | return 0, err
38 | }
39 | return res.RowsAffected()
40 | }
41 |
--------------------------------------------------------------------------------
/internal/grpcerr/grpc.go:
--------------------------------------------------------------------------------
1 | package grpcerr
2 |
3 | import (
4 | "context"
5 |
6 | "google.golang.org/grpc"
7 | "google.golang.org/grpc/status"
8 | )
9 |
10 | func UnaryServerInterceptor() grpc.UnaryServerInterceptor {
11 | return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (res interface{}, err error) {
12 | res, err = handler(ctx, req)
13 | return res, grpcError(err)
14 | }
15 | }
16 |
17 | func StreamServerInterceptor() grpc.StreamServerInterceptor {
18 | return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
19 | return grpcError(handler(srv, ss))
20 | }
21 | }
22 |
23 | func grpcError(err error) error {
24 | ert, ok := err.(*Error)
25 | if !ok {
26 | return err
27 | }
28 |
29 | if ert.Msg != "" {
30 | sts, err := status.Newf(ert.Code, "%s: %s", ert.Msg, ert.Err).WithDetails(ert.Details...)
31 | // error is not nil only if code was OK
32 | if err != nil {
33 | return status.Newf(ert.Code, "%s: %s", ert.Msg, ert.Err).Err()
34 | }
35 |
36 | return sts.Err()
37 | } else {
38 | sts, err := status.Newf(ert.Code, "%s", ert.Err).WithDetails(ert.Details...)
39 | // error is not nil only if code was OK
40 | if err != nil {
41 | return status.Newf(ert.Code, "%s", ert.Err).Err()
42 | }
43 |
44 | return sts.Err()
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/internal/password/password.go:
--------------------------------------------------------------------------------
1 | package password
2 |
3 | import (
4 | "errors"
5 |
6 | "golang.org/x/crypto/bcrypt"
7 | )
8 |
9 | var (
10 | // ErrBCryptCostOutOfRange can be returned by NewBCryptHasher if provided cost is not between min and max.
11 | ErrBCryptCostOutOfRange = errors.New("password: bcrypt cost out of range")
12 | )
13 |
14 | // Hasher define set of methods that object needs to implement to be considered as a hasher.
15 | type Hasher interface {
16 | Hash([]byte) ([]byte, error)
17 | Compare([]byte, []byte) bool
18 | }
19 |
20 | // BCryptHasher hasher that use BCrypt algorithm to secure password.
21 | type BCryptHasher struct {
22 | cost int
23 | }
24 |
25 | // NewBCryptHasher allocates new NewBCryptHasher.
26 | // If cost is not between min and max value it returns an error.
27 | func NewBCryptHasher(cost int) (Hasher, error) {
28 | if bcrypt.MinCost > cost || cost > bcrypt.MaxCost {
29 | return nil, ErrBCryptCostOutOfRange
30 | }
31 |
32 | return &BCryptHasher{cost: cost}, nil
33 | }
34 |
35 | // Hash implements Hasher interface.
36 | func (bh BCryptHasher) Hash(plainPassword []byte) ([]byte, error) {
37 | return bcrypt.GenerateFromPassword(plainPassword, bh.cost)
38 | }
39 |
40 | // Compare implements Hasher interface.
41 | func (bh BCryptHasher) Compare(hashedPassword, plainPassword []byte) bool {
42 | err := bcrypt.CompareHashAndPassword(hashedPassword, plainPassword)
43 | return err == nil
44 | }
45 |
--------------------------------------------------------------------------------
/internal/mapping/group.go:
--------------------------------------------------------------------------------
1 | package mapping
2 |
3 | import (
4 | "github.com/golang/protobuf/ptypes"
5 | pbts "github.com/golang/protobuf/ptypes/timestamp"
6 | charonrpc "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1"
7 | "github.com/piotrkowalczuk/charon/internal/model"
8 | )
9 |
10 | // ReverseGroup maps internal entity struct into protobuf message used by a client.
11 | func ReverseGroup(ent *model.GroupEntity) (*charonrpc.Group, error) {
12 | var (
13 | err error
14 | createdAt, updatedAt *pbts.Timestamp
15 | )
16 |
17 | if createdAt, err = ptypes.TimestampProto(ent.CreatedAt); err != nil {
18 | return nil, err
19 | }
20 | if ent.UpdatedAt.Valid {
21 | if updatedAt, err = ptypes.TimestampProto(ent.UpdatedAt.Time); err != nil {
22 | return nil, err
23 | }
24 | }
25 |
26 | return &charonrpc.Group{
27 | Id: ent.ID,
28 | Name: ent.Name,
29 | Description: ent.Description.Chars,
30 | CreatedAt: createdAt,
31 | CreatedBy: &ent.CreatedBy,
32 | UpdatedAt: updatedAt,
33 | UpdatedBy: &ent.UpdatedBy,
34 | }, nil
35 | }
36 |
37 | // ReverseGroups does same thing like ReverseGroup but operate on slices.
38 | func ReverseGroups(in []*model.GroupEntity) ([]*charonrpc.Group, error) {
39 | res := make([]*charonrpc.Group, 0, len(in))
40 | for _, ent := range in {
41 | msg, err := ReverseGroup(ent)
42 | if err != nil {
43 | return nil, err
44 | }
45 | res = append(res, msg)
46 | }
47 |
48 | return res, nil
49 | }
50 |
--------------------------------------------------------------------------------
/internal/charonctl/console.go:
--------------------------------------------------------------------------------
1 | package charonctl
2 |
3 | import (
4 | "context"
5 |
6 | charonrpc "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1"
7 | "github.com/piotrkowalczuk/mnemosyne"
8 | "google.golang.org/grpc"
9 | "google.golang.org/grpc/metadata"
10 | )
11 |
12 | type ConsoleOpts struct {
13 | Conn *grpc.ClientConn
14 | Username string
15 | Password string
16 | }
17 |
18 | type Console struct {
19 | Ctx context.Context
20 | consoleRegisterUser
21 | consoleObtainRefreshToken
22 | }
23 |
24 | func NewConsole(opts ConsoleOpts) (*Console, error) {
25 | auth := charonrpc.NewAuthClient(opts.Conn)
26 | user := charonrpc.NewUserManagerClient(opts.Conn)
27 | refreshToken := charonrpc.NewRefreshTokenManagerClient(opts.Conn)
28 |
29 | c := &Console{
30 | Ctx: context.Background(),
31 | consoleObtainRefreshToken: consoleObtainRefreshToken{
32 | refreshToken: refreshToken,
33 | },
34 | consoleRegisterUser: consoleRegisterUser{
35 | user: user,
36 | },
37 | }
38 |
39 | ctx := context.Background()
40 |
41 | if opts.Password != "" || opts.Username != "" {
42 | resp, err := auth.Login(context.Background(), &charonrpc.LoginRequest{
43 | Username: opts.Username,
44 | Password: opts.Password,
45 | })
46 | if err != nil {
47 | return nil, &Error{
48 | Msg: "(initial) login failure",
49 | Err: err,
50 | }
51 | }
52 |
53 | c.Ctx = metadata.NewOutgoingContext(ctx, metadata.Pairs(mnemosyne.AccessTokenMetadataKey, resp.Value))
54 | }
55 |
56 | return c, nil
57 | }
58 |
--------------------------------------------------------------------------------
/internal/mapping/user.go:
--------------------------------------------------------------------------------
1 | package mapping
2 |
3 | import (
4 | "github.com/golang/protobuf/ptypes"
5 | pbts "github.com/golang/protobuf/ptypes/timestamp"
6 | charonrpc "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1"
7 | "github.com/piotrkowalczuk/charon/internal/model"
8 | )
9 |
10 | func ReverseUser(ent *model.UserEntity) (*charonrpc.User, error) {
11 | var (
12 | err error
13 | createdAt, updatedAt *pbts.Timestamp
14 | )
15 |
16 | if !ent.CreatedAt.IsZero() {
17 | if createdAt, err = ptypes.TimestampProto(ent.CreatedAt); err != nil {
18 | return nil, err
19 | }
20 | }
21 | if ent.UpdatedAt.Valid {
22 | if updatedAt, err = ptypes.TimestampProto(ent.UpdatedAt.Time); err != nil {
23 | return nil, err
24 | }
25 | }
26 |
27 | return &charonrpc.User{
28 | Id: ent.ID,
29 | Username: ent.Username,
30 | FirstName: ent.FirstName,
31 | LastName: ent.LastName,
32 | IsSuperuser: ent.IsSuperuser,
33 | IsActive: ent.IsActive,
34 | IsStaff: ent.IsStaff,
35 | IsConfirmed: ent.IsConfirmed,
36 | CreatedAt: createdAt,
37 | CreatedBy: &ent.CreatedBy,
38 | UpdatedAt: updatedAt,
39 | UpdatedBy: &ent.UpdatedBy,
40 | }, nil
41 | }
42 |
43 | func ReverseUsers(in []*model.UserEntity) ([]*charonrpc.User, error) {
44 | res := make([]*charonrpc.User, 0, len(in))
45 | for _, ent := range in {
46 | msg, err := ReverseUser(ent)
47 | if err != nil {
48 | return nil, err
49 | }
50 | res = append(res, msg)
51 | }
52 |
53 | return res, nil
54 | }
55 |
--------------------------------------------------------------------------------
/.circleci/scripts/generate.sh:
--------------------------------------------------------------------------------
1 | #@IgnoreInspection BashAddShebang
2 | SERVICE="charon"
3 | PROTO_INCLUDE="-I=./tmp/pb -I=./vendor/github.com/piotrkowalczuk -I=${GOPATH}/src"
4 |
5 | : ${PROTOC:="${PWD}/tmp/bin/protoc"}
6 |
7 | protobufs=(
8 | "rpc/${SERVICE}d/v1"
9 | )
10 | for protobuf in "${protobufs[@]}"
11 | do
12 | case $1 in
13 | lint)
14 | ${PROTOC} ${PROTO_INCLUDE} --lint_out=. ${PWD}/pb/${protobuf}/*.proto
15 | ;;
16 | python)
17 | python -m grpc_tools.protoc ${PROTO_INCLUDE} --python_out=publish/python --grpc_python_out=publish/python ${PWD}/pb/${protobuf}/*.proto
18 | cp publish/python/github.com/piotrkowalczuk/charon/pb/${protobuf}/* publish/python/github/com/piotrkowalczuk/charon/pb/${protobuf}/
19 | rm -rf publish/python/github.com
20 | ;;
21 | java)
22 | rm -rf ./publish/java
23 | mkdir -p ./publish/java
24 | ${PROTOC} ${PROTO_INCLUDE} --java_out=./publish/java ${PWD}/pb/${protobuf}/*.proto
25 | ;;
26 | golang | go)
27 | ${PROTOC} ${PROTO_INCLUDE} --go_out=plugins=grpc:${GOPATH}/src ${PWD}/pb/${protobuf}/*.proto
28 | mockery -case=underscore -dir=./pb/${protobuf} -all -outpkg=$(basename $(dirname "./pb/${protobuf}mock"))mock -output=./pb/${protobuf}mock
29 | goimports -w ${PWD}/pb
30 | ;;
31 | *)
32 | echo "code generation failure due to unknown language: ${1}"
33 | exit 1
34 | ;;
35 | esac
36 | done
--------------------------------------------------------------------------------
/cmd/charonctl/service.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "os"
7 |
8 | charonrpc "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1"
9 | "github.com/piotrkowalczuk/mnemosyne"
10 | "google.golang.org/grpc"
11 | "google.golang.org/grpc/metadata"
12 | "google.golang.org/grpc/status"
13 | )
14 |
15 | type client struct {
16 | auth charonrpc.AuthClient
17 | user charonrpc.UserManagerClient
18 | group charonrpc.GroupManagerClient
19 | permission charonrpc.PermissionManagerClient
20 | }
21 |
22 | func initClient(addr string) (c *client, ctx context.Context) {
23 | conn, err := grpc.Dial(addr, grpc.WithInsecure(), grpc.WithUserAgent("charonctl"))
24 | if err != nil {
25 | fmt.Printf("charond connection failure to %s with error: %s\n", addr, status.Convert(err).Message())
26 | os.Exit(1)
27 | }
28 |
29 | c = &client{
30 | auth: charonrpc.NewAuthClient(conn),
31 | user: charonrpc.NewUserManagerClient(conn),
32 | group: charonrpc.NewGroupManagerClient(conn),
33 | permission: charonrpc.NewPermissionManagerClient(conn),
34 | }
35 | ctx = context.Background()
36 |
37 | if config.auth.enabled {
38 | resp, err := c.auth.Login(context.Background(), &charonrpc.LoginRequest{
39 | Username: config.auth.username,
40 | Password: config.auth.password,
41 | })
42 | if err != nil {
43 | fmt.Printf("(initial) login failure: %s\n", status.Convert(err).Message())
44 | os.Exit(1)
45 | }
46 |
47 | ctx = metadata.NewOutgoingContext(ctx, metadata.Pairs(mnemosyne.AccessTokenMetadataKey, resp.Value))
48 | }
49 |
50 | return
51 | }
52 |
--------------------------------------------------------------------------------
/internal/model/suite_postgres_test.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "database/sql"
5 | "testing"
6 |
7 | "github.com/lib/pq"
8 | "go.uber.org/zap"
9 | )
10 |
11 | type postgresSuite struct {
12 | logger *zap.Logger
13 | db *sql.DB
14 | repository repositories
15 | }
16 |
17 | func (ps *postgresSuite) setup(t *testing.T) {
18 | if testing.Short() {
19 | t.Skip("postgres suite ignored in short mode")
20 | }
21 |
22 | var err error
23 |
24 | ps.logger = zap.L()
25 | ps.db, err = initPostgres(testPostgresAddress, true, ps.logger)
26 | if err != nil {
27 | t.Fatalf("postgres connection (%s) error: %s", testPostgresAddress, err.Error())
28 | }
29 |
30 | ps.repository = newRepositories(ps.db)
31 | }
32 |
33 | func (ps *postgresSuite) teardown(t *testing.T) {
34 | var err error
35 |
36 | if err = teardownDatabase(ps.db); err != nil {
37 | t.Fatalf("postgres suite database teardown error: %s", err.Error())
38 | }
39 | if err = ps.db.Close(); err != nil {
40 | t.Fatalf("postgres suite teardown database connection error: %s", err.Error())
41 | }
42 | }
43 |
44 | func assertf(t *testing.T, is bool, msg string, args ...interface{}) bool {
45 | if !is {
46 | t.Errorf(msg, args...)
47 | }
48 |
49 | return is
50 | }
51 |
52 | func assert(t *testing.T, is bool, msg string) bool {
53 | if !is {
54 | t.Errorf(msg)
55 | }
56 |
57 | return is
58 | }
59 |
60 | func assertfNullTime(t *testing.T, tm pq.NullTime, msg string, args ...interface{}) bool {
61 | if !tm.Valid || tm.Time.IsZero() {
62 | t.Errorf(msg, args...)
63 | return false
64 | }
65 |
66 | return true
67 | }
68 |
--------------------------------------------------------------------------------
/pb/rpc/charond/v1/permission.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package charon.rpc.charond.v1;
4 |
5 | option go_package = "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1;charond";
6 | option java_multiple_files = true;
7 | option java_package = "com.github.charon.rpc.charond.v1";
8 |
9 | import "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1/common.proto";
10 | import "qtypes/qtypes.proto";
11 | import "ntypes/ntypes.proto";
12 |
13 | service PermissionManager {
14 | rpc Register(RegisterPermissionsRequest) returns (RegisterPermissionsResponse) {};
15 | rpc List(ListPermissionsRequest) returns (ListPermissionsResponse) {};
16 | rpc Get(GetPermissionRequest) returns (GetPermissionResponse) {};
17 | }
18 |
19 | message RegisterPermissionsRequest {
20 | repeated string permissions = 1;
21 | }
22 |
23 | message RegisterPermissionsResponse {
24 | int64 created = 1;
25 | int64 removed = 2;
26 | int64 untouched = 3;
27 | }
28 |
29 | message ListPermissionsRequest {
30 | reserved 6 to 99;
31 |
32 | qtypes.String subsystem = 1;
33 | qtypes.String module = 2;
34 | qtypes.String action = 3;
35 | qtypes.Timestamp created_at = 4;
36 | qtypes.Int64 created_by = 5;
37 |
38 | ntypes.Int64 offset = 100;
39 | ntypes.Int64 limit = 101;
40 | map sort = 102 [deprecated=true];
41 | repeated Order order_by = 103;
42 | }
43 |
44 | message ListPermissionsResponse {
45 | repeated string permissions = 1;
46 | }
47 |
48 | message GetPermissionRequest {
49 | int64 id = 1;
50 | }
51 |
52 | message GetPermissionResponse {
53 | string permission = 1;
54 | }
55 |
--------------------------------------------------------------------------------
/internal/charond/handler_get_permission.go:
--------------------------------------------------------------------------------
1 | package charond
2 |
3 | import (
4 | "context"
5 | "database/sql"
6 |
7 | "github.com/piotrkowalczuk/charon"
8 | charonrpc "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1"
9 | "github.com/piotrkowalczuk/charon/internal/grpcerr"
10 | "github.com/piotrkowalczuk/charon/internal/session"
11 |
12 | "google.golang.org/grpc/codes"
13 | )
14 |
15 | type getPermissionHandler struct {
16 | *handler
17 | }
18 |
19 | func (gph *getPermissionHandler) Get(ctx context.Context, req *charonrpc.GetPermissionRequest) (*charonrpc.GetPermissionResponse, error) {
20 | if req.Id < 1 {
21 | return nil, grpcerr.E(codes.InvalidArgument, "permission id needs to be greater than zero")
22 | }
23 |
24 | act, err := gph.Actor(ctx)
25 | if err != nil {
26 | return nil, err
27 | }
28 | if err = gph.firewall(req, act); err != nil {
29 | return nil, err
30 | }
31 |
32 | permission, err := gph.repository.permission.FindOneByID(ctx, req.Id)
33 | if err != nil {
34 | if err == sql.ErrNoRows {
35 | return nil, grpcerr.E(codes.NotFound, "permission does not exists")
36 | }
37 | return nil, grpcerr.E(codes.Internal, "permission cannot be fetched", err)
38 | }
39 |
40 | return &charonrpc.GetPermissionResponse{
41 | Permission: permission.Permission().String(),
42 | }, nil
43 | }
44 |
45 | func (gph *getPermissionHandler) firewall(req *charonrpc.GetPermissionRequest, act *session.Actor) error {
46 | if act.User.IsSuperuser {
47 | return nil
48 | }
49 | if act.Permissions.Contains(charon.PermissionCanRetrieve) {
50 | return nil
51 | }
52 |
53 | return grpcerr.E(codes.PermissionDenied, "permission cannot be retrieved, missing permission")
54 | }
55 |
--------------------------------------------------------------------------------
/internal/service/logger/logger.go:
--------------------------------------------------------------------------------
1 | package logger
2 |
3 | import (
4 | "errors"
5 |
6 | "github.com/piotrkowalczuk/zapstackdriver"
7 | "go.uber.org/zap"
8 | "go.uber.org/zap/zapcore"
9 | )
10 |
11 | const (
12 | production = "production"
13 | development = "development"
14 | stackdriver = "stackdriver"
15 | )
16 |
17 | type Opts struct {
18 | Environment string
19 | Level string
20 | }
21 |
22 | // Init allocates new logger based on given options.
23 | func Init(service, version string, opts Opts) (logger *zap.Logger, err error) {
24 | if service == "" {
25 | return nil, errors.New("logger: service name is missing, logger cannot be initialized")
26 | }
27 | if version == "" {
28 | return nil, errors.New("logger: service version is missing, logger cannot be initialized")
29 | }
30 |
31 | var (
32 | zapCfg zap.Config
33 | zapOpts []zap.Option
34 | lvl zapcore.Level
35 | )
36 | switch opts.Environment {
37 | case production:
38 | zapCfg = zap.NewProductionConfig()
39 | case stackdriver:
40 | zapCfg = zapstackdriver.NewStackdriverConfig()
41 | case development:
42 | zapCfg = zap.NewDevelopmentConfig()
43 | default:
44 | zapCfg = zap.NewProductionConfig()
45 | }
46 |
47 | if err = lvl.Set(opts.Level); err != nil {
48 | return nil, err
49 | }
50 | zapCfg.Level.SetLevel(lvl)
51 |
52 | logger, err = zapCfg.Build(zapOpts...)
53 | if err != nil {
54 | return nil, err
55 | }
56 | logger = logger.With(zap.Object("serviceContext", &zapstackdriver.ServiceContext{
57 | Service: service,
58 | Version: version,
59 | }))
60 | logger.Info("logger has been initialized", zap.String("environment", opts.Environment))
61 |
62 | return logger, nil
63 | }
64 |
--------------------------------------------------------------------------------
/internal/charond/handler_is_authenticated.go:
--------------------------------------------------------------------------------
1 | package charond
2 |
3 | import (
4 | "context"
5 | "database/sql"
6 |
7 | "github.com/golang/protobuf/ptypes/wrappers"
8 | charonrpc "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1"
9 | "github.com/piotrkowalczuk/charon/internal/grpcerr"
10 | "github.com/piotrkowalczuk/charon/internal/session"
11 | "github.com/piotrkowalczuk/mnemosyne/mnemosynerpc"
12 | "google.golang.org/grpc/codes"
13 | "google.golang.org/grpc/status"
14 | )
15 |
16 | type isAuthenticatedHandler struct {
17 | *handler
18 | }
19 |
20 | func (iah *isAuthenticatedHandler) IsAuthenticated(ctx context.Context, req *charonrpc.IsAuthenticatedRequest) (*wrappers.BoolValue, error) {
21 | if req.AccessToken == "" {
22 | return nil, grpcerr.E(codes.InvalidArgument, "authentication status cannot be checked, missing access token")
23 | }
24 |
25 | ses, err := iah.session.Get(ctx, &mnemosynerpc.GetRequest{AccessToken: req.AccessToken})
26 | if err != nil {
27 | if st, ok := status.FromError(err); ok {
28 | if st.Code() == codes.NotFound {
29 | return &wrappers.BoolValue{Value: false}, nil
30 | }
31 | }
32 | return nil, grpcerr.E(codes.Internal, "session cannot be fetched", err)
33 | }
34 | uid, err := session.ActorID(ses.Session.SubjectId).UserID()
35 | if err != nil {
36 | return nil, grpcerr.E(codes.Internal, "invalid actor id", err)
37 | }
38 | exists, err := iah.repository.user.Exists(ctx, uid)
39 | if err != nil {
40 | if err == sql.ErrNoRows {
41 | return &wrappers.BoolValue{Value: false}, nil
42 | }
43 | return nil, grpcerr.E(codes.Internal, "user cannot be fetched", err)
44 | }
45 |
46 | return &wrappers.BoolValue{Value: exists}, nil
47 | }
48 |
--------------------------------------------------------------------------------
/internal/model/group_permissions_test.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "context"
5 | "testing"
6 |
7 | "github.com/piotrkowalczuk/ntypes"
8 | )
9 |
10 | var (
11 | groupPermissionsTestFixtures = []*GroupEntity{
12 | {
13 | ID: 1,
14 | Name: "group_1",
15 | Description: ntypes.String{Chars: "first group", Valid: true},
16 | Permissions: []*PermissionEntity{
17 | {
18 | ID: 1,
19 | Subsystem: "subsystem_1",
20 | Module: "module_1",
21 | Action: "action_1",
22 | },
23 | },
24 | },
25 | {
26 | ID: 2,
27 | Name: "group_2",
28 | Description: ntypes.String{Chars: "second group", Valid: true},
29 | Permissions: []*PermissionEntity{
30 | {
31 | ID: 2,
32 | Subsystem: "subsystem_2",
33 | Module: "module_2",
34 | Action: "action_2",
35 | },
36 | },
37 | },
38 | }
39 | )
40 |
41 | type groupPermissionsFixtures struct {
42 | got, given GroupPermissionsEntity
43 | }
44 |
45 | func loadGroupPermissionsFixtures(t *testing.T, r GroupPermissionsProvider, f []*GroupPermissionsEntity) chan groupPermissionsFixtures {
46 | data := make(chan groupPermissionsFixtures, 1)
47 |
48 | go func() {
49 | for _, given := range f {
50 | entity, err := r.Insert(context.TODO(), given)
51 | if err != nil {
52 | t.Errorf("group permission cannot be created, unexpected error: %s", err.Error())
53 | continue
54 | } else {
55 | t.Log("group permission has been created")
56 | }
57 |
58 | data <- groupPermissionsFixtures{
59 | got: *entity,
60 | given: *given,
61 | }
62 | }
63 |
64 | close(data)
65 | }()
66 |
67 | return data
68 | }
69 |
--------------------------------------------------------------------------------
/internal/charonctl/console_obtain_refresh_token.go:
--------------------------------------------------------------------------------
1 | package charonctl
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "time"
7 |
8 | "github.com/golang/protobuf/ptypes"
9 | "github.com/golang/protobuf/ptypes/timestamp"
10 | charonrpc "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1"
11 | "github.com/piotrkowalczuk/ntypes"
12 | )
13 |
14 | type ObtainRefreshTokenArg struct {
15 | Notes string
16 | ExpireAfter time.Duration
17 | }
18 |
19 | type consoleObtainRefreshToken struct {
20 | refreshToken charonrpc.RefreshTokenManagerClient
21 | }
22 |
23 | func (cort *consoleObtainRefreshToken) ObtainRefreshToken(ctx context.Context, arg *ObtainRefreshTokenArg) error {
24 | var (
25 | expireAt *timestamp.Timestamp
26 | err error
27 | )
28 | if arg.ExpireAfter != time.Duration(0) {
29 | if expireAt, err = ptypes.TimestampProto(time.Now().Add(arg.ExpireAfter)); err != nil {
30 | return err
31 | }
32 | }
33 |
34 | _, err = cort.refreshToken.Create(ctx, &charonrpc.CreateRefreshTokenRequest{
35 | Notes: &ntypes.String{Chars: arg.Notes, Valid: arg.Notes != ""},
36 | ExpireAt: expireAt,
37 | })
38 | if err != nil {
39 | return err
40 | }
41 |
42 | res, err := cort.refreshToken.List(ctx, &charonrpc.ListRefreshTokensRequest{})
43 | if err != nil {
44 | return err
45 | }
46 | for _, rt := range res.RefreshTokens {
47 | eat, err := ptypes.Timestamp(rt.ExpireAt)
48 | if err != nil {
49 | fmt.Printf("%-36s ", "never")
50 | } else {
51 | fmt.Printf("%-36s ", eat.String())
52 | }
53 |
54 | fmt.Printf("%s", rt.Token)
55 | if rt.Notes != nil && rt.Notes.Valid {
56 | fmt.Printf(" - %s", rt.Notes.StringOr(""))
57 | }
58 | fmt.Print("\n")
59 | }
60 | return nil
61 | }
62 |
--------------------------------------------------------------------------------
/internal/charond/handler_belongs_to.go:
--------------------------------------------------------------------------------
1 | package charond
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/golang/protobuf/ptypes/wrappers"
7 | "github.com/piotrkowalczuk/charon"
8 | charonrpc "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1"
9 | "github.com/piotrkowalczuk/charon/internal/grpcerr"
10 | "github.com/piotrkowalczuk/charon/internal/session"
11 | "google.golang.org/grpc/codes"
12 | )
13 |
14 | type belongsToHandler struct {
15 | *handler
16 | }
17 |
18 | func (bth *belongsToHandler) BelongsTo(ctx context.Context, req *charonrpc.BelongsToRequest) (*wrappers.BoolValue, error) {
19 | if req.GroupId < 1 {
20 | return nil, grpcerr.E(codes.InvalidArgument, "group id needs to be greater than zero")
21 | }
22 | if req.UserId < 1 {
23 | return nil, grpcerr.E(codes.InvalidArgument, "user id needs to be greater than zero")
24 | }
25 |
26 | act, err := bth.Actor(ctx)
27 | if err != nil {
28 | return nil, err
29 | }
30 | if err = bth.firewall(req, act); err != nil {
31 | return nil, err
32 | }
33 |
34 | belongs, err := bth.repository.userGroups.Exists(ctx, req.UserId, req.GroupId)
35 | if err != nil {
36 | return nil, grpcerr.E(codes.Internal, "user group fetch failure", err)
37 | }
38 |
39 | return &wrappers.BoolValue{Value: belongs}, nil
40 | }
41 |
42 | func (bth *belongsToHandler) firewall(req *charonrpc.BelongsToRequest, act *session.Actor) error {
43 | if act.User.ID == req.UserId {
44 | return nil
45 | }
46 | if act.User.IsSuperuser {
47 | return nil
48 | }
49 | if act.Permissions.Contains(charon.UserGroupCanCheckBelongingAsStranger) {
50 | return nil
51 | }
52 |
53 | return grpcerr.E(codes.PermissionDenied, "group belonging cannot be checked, missing permission")
54 | }
55 |
--------------------------------------------------------------------------------
/internal/model/user_permissions_test.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "context"
5 | "testing"
6 | )
7 |
8 | var (
9 | userPermissionsTestFixtures = []*UserEntity{
10 | {
11 | ID: 1,
12 | Username: "user1@example.com",
13 | FirstName: "first_name_1",
14 | LastName: "last_name_1",
15 | Password: []byte("0123456789"),
16 | Permissions: []*PermissionEntity{
17 | {
18 | ID: 1,
19 | Subsystem: "subsystem_1",
20 | Module: "module_1",
21 | Action: "action_1",
22 | },
23 | },
24 | },
25 | {
26 | ID: 2,
27 | Username: "user2@example.com",
28 | FirstName: "first_name_2",
29 | LastName: "last_name_2",
30 | Password: []byte("9876543210"),
31 | Permissions: []*PermissionEntity{
32 | {
33 | ID: 2,
34 | Subsystem: "subsystem_2",
35 | Module: "module_2",
36 | Action: "action_2",
37 | },
38 | },
39 | },
40 | }
41 | )
42 |
43 | type userPermissionsFixtures struct {
44 | got, given UserPermissionsEntity
45 | }
46 |
47 | func loadUserPermissionsFixtures(t *testing.T, r UserPermissionsProvider, f []*UserPermissionsEntity) chan userPermissionsFixtures {
48 | data := make(chan userPermissionsFixtures, 1)
49 |
50 | go func() {
51 | for _, given := range f {
52 | entity, err := r.Insert(context.TODO(), given)
53 | if err != nil {
54 | t.Errorf("user permission cannot be created, unexpected error: %s", err.Error())
55 | continue
56 | } else {
57 | t.Log("user permission has been created")
58 | }
59 |
60 | data <- userPermissionsFixtures{
61 | got: *entity,
62 | given: *given,
63 | }
64 | }
65 |
66 | close(data)
67 | }()
68 |
69 | return data
70 | }
71 |
--------------------------------------------------------------------------------
/internal/charond/handler_is_granted.go:
--------------------------------------------------------------------------------
1 | package charond
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/golang/protobuf/ptypes/wrappers"
7 | "github.com/piotrkowalczuk/charon"
8 | charonrpc "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1"
9 | "github.com/piotrkowalczuk/charon/internal/grpcerr"
10 | "github.com/piotrkowalczuk/charon/internal/session"
11 |
12 | "google.golang.org/grpc/codes"
13 | )
14 |
15 | type isGrantedHandler struct {
16 | *handler
17 | }
18 |
19 | func (ig *isGrantedHandler) IsGranted(ctx context.Context, req *charonrpc.IsGrantedRequest) (*wrappers.BoolValue, error) {
20 | if req.Permission == "" {
21 | return nil, grpcerr.E(codes.InvalidArgument, "permission cannot be empty")
22 | }
23 | if req.UserId < 1 {
24 | return nil, grpcerr.E(codes.InvalidArgument, "user id needs to be greater than zero")
25 | }
26 |
27 | act, err := ig.Actor(ctx)
28 | if err != nil {
29 | return nil, err
30 | }
31 | if err = ig.firewall(req, act); err != nil {
32 | return nil, err
33 | }
34 |
35 | granted, err := ig.repository.user.IsGranted(ctx, req.UserId, charon.Permission(req.Permission))
36 | if err != nil {
37 | return nil, grpcerr.E(codes.Internal, "is granted repository call failure", err)
38 | }
39 |
40 | return &wrappers.BoolValue{Value: granted}, nil
41 | }
42 |
43 | func (ig *isGrantedHandler) firewall(req *charonrpc.IsGrantedRequest, act *session.Actor) error {
44 | if act.User.ID == req.UserId {
45 | return nil
46 | }
47 | if act.User.IsSuperuser {
48 | return nil
49 | }
50 | if act.Permissions.Contains(charon.UserPermissionCanCheckGrantingAsStranger) {
51 | return nil
52 | }
53 |
54 | return grpcerr.E(codes.PermissionDenied, "group granting cannot be checked, missing permission")
55 | }
56 |
--------------------------------------------------------------------------------
/data/test-selfsigned.key:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | MIIEowIBAAKCAQEA7Sklr3QwUhvAetYASNTip5w7dL+xb+VYv4ErAJOGW4uGrun1
3 | kTbR9JVz0L3gR3Ss4htNcPCunzSeWWwvO82+FnUGL1uzRByHWaXMDo8IYEPpAjl9
4 | 1rMaV+4fvQGqhcUyK+MKoFVb0E1xcd+1CbjWerV91IqTa50LCmQj1swQfirEGacP
5 | 19JTNkm74QcqVK1vYPAVFsy0ng2kMBV//52s5wOgj2PpDcaICccy6QIuXjAkO3u9
6 | QZDhai4bot+lBjP5j/0F98LaYKaHmVmpv76TxrmBHQ6/AHSHQ+rI98K1Nj+SUmJ9
7 | 8zd9/Qy5vyqtx4aSYTOqMsvV1l9WpFAsOea+xQIDAQABAoIBAD45XIzjVEZGx8Ky
8 | 4VI6oNlPMX5ZSUnNh/J/BnRZQJhGTGkaM3cNRhsBF2j+WJkG8NFGTpRCYd6dpKpb
9 | qyzqt2QXAi9sxOUrAwwvZxGuz4jKaJlP0keBqHjHnoYtqLr+WUKQiFo26ycFGq3A
10 | /zP0zjlV4xTf4vwKMTphudGCqxUezBNIsB0G/6XALY7JCgQ2ABsW4LVojcU3Iktv
11 | EPEEeThrgiqbclxvO1HpecBrKUIDwnp2LBA8Yb23stWjHhkDiJe/FbPJh5t4vdzS
12 | Q+bk9XqQKXRz3ULZJEi7zuuUVEs2z+IoHVZBavMYReEdq3Qrn/EFvouaEL0zIbWM
13 | l9WqKQUCgYEA+dIZHNXNPKTBQHI7wn2IXxZ3IaNA6Mwsz/d2fgYKMWl7nP37UXAb
14 | sFntMGPAr01Ca3VWUcD5uIIs1XqMV6UMQkMF0XXcfgRATVmp6xFqkc5vBVQsAAD8
15 | lMZkeikZ9h9+Y4Zym4n7dnrmbJxk8sUuhMyqcnm6iFCZVhcTsm6Ak98CgYEA8wbi
16 | Y2q50auazgRwubiQgYcvqP3xBHFJN2WbgOEfrfoxZsPt8enmdTrotZh2dTzGDXvU
17 | ikyy8PSbzcSL4Xveqo1kGsDExHkZKXn4p96/BHabRAyyL38NmZSslxU9XF9+/TR7
18 | H/9W5jI6LV+zdFK1XqLRfZ399UrCV08iOPA3odsCgYAoXjjMngfCCuVnYo4hiWNl
19 | 6h1qBBVTCNsc5+Hvz04KWf8tiST5LeJrhrx7G0NhkFxxPM6r+0De/bn87QaMixEG
20 | DAp+pEry2gEB/sEkSPYthWwPMmDBx2cJK13FF6soSEKGFo2icJN/u7BMUKFHUXGI
21 | 6AGK6fyoVk3QyX3XUV/ZhQKBgQDJmTNhXx5IBP+E2QAdwDH3kQoDOKyfj845qwsF
22 | LTrXWo1yfyO6otou8zApvBDADisI8mkMVLW31mIMnnefE99RQbsBylkv1nj+BBU2
23 | RDvW2wDPwWxqpA3HEiLdrZvaLcmtB8T/dRREHvRNwW6fFyEnIQ+BEfeibGKshJOS
24 | AgtUMwKBgHRvNHh84wVtegWqXAcXAYYhVqwAjwj1jnzo2IxNm6cKZbXTLKxjXUdS
25 | GIyY/3y4lVdxp+YNVSMROX6YNnGVP6GtgQzQ3ShWuK9uXvNL628EFeXYq8ydD4Jr
26 | uDfX1gdLIjoqUpy9aQ/1fyCFftyKxapkDVPqq+1QKZora+oyOX6X
27 | -----END RSA PRIVATE KEY-----
28 |
--------------------------------------------------------------------------------
/internal/charond/handler_list_user_groups.go:
--------------------------------------------------------------------------------
1 | package charond
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/piotrkowalczuk/charon"
7 | charonrpc "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1"
8 | "github.com/piotrkowalczuk/charon/internal/grpcerr"
9 | "github.com/piotrkowalczuk/charon/internal/mapping"
10 | "github.com/piotrkowalczuk/charon/internal/session"
11 |
12 | "google.golang.org/grpc/codes"
13 | )
14 |
15 | type listUserGroupsHandler struct {
16 | *handler
17 | }
18 |
19 | func (lugh *listUserGroupsHandler) ListGroups(ctx context.Context, req *charonrpc.ListUserGroupsRequest) (*charonrpc.ListUserGroupsResponse, error) {
20 | if req.Id <= 0 {
21 | return nil, grpcerr.E(codes.InvalidArgument, "missing user id")
22 | }
23 | act, err := lugh.Actor(ctx)
24 | if err != nil {
25 | return nil, err
26 | }
27 | if err = lugh.firewall(req, act); err != nil {
28 | return nil, err
29 | }
30 |
31 | ents, err := lugh.repository.group.FindByUserID(ctx, req.Id)
32 | if err != nil {
33 | return nil, grpcerr.E(codes.Internal, "find groups by user id query failed", err)
34 | }
35 |
36 | msg, err := mapping.ReverseGroups(ents)
37 | if err != nil {
38 | return nil, grpcerr.E(codes.Internal, "user group entities mapping failure", err)
39 | }
40 |
41 | return &charonrpc.ListUserGroupsResponse{Groups: msg}, nil
42 | }
43 |
44 | func (lugh *listUserGroupsHandler) firewall(req *charonrpc.ListUserGroupsRequest, act *session.Actor) error {
45 | switch {
46 | case act.User.IsSuperuser:
47 | return nil
48 | case act.User.ID == req.Id:
49 | return nil
50 | case act.Permissions.Contains(charon.UserGroupCanRetrieve):
51 | return nil
52 | }
53 |
54 | return grpcerr.E(codes.PermissionDenied, "list of user groups cannot be retrieved, missing permission")
55 | }
56 |
--------------------------------------------------------------------------------
/internal/model/modelmock/user_permissions_provider.go:
--------------------------------------------------------------------------------
1 | // Code generated by mockery v1.0.0. DO NOT EDIT.
2 |
3 | package modelmock
4 |
5 | import context "context"
6 | import mock "github.com/stretchr/testify/mock"
7 | import model "github.com/piotrkowalczuk/charon/internal/model"
8 |
9 | // UserPermissionsProvider is an autogenerated mock type for the UserPermissionsProvider type
10 | type UserPermissionsProvider struct {
11 | mock.Mock
12 | }
13 |
14 | // DeleteByUserID provides a mock function with given fields: _a0, _a1
15 | func (_m *UserPermissionsProvider) DeleteByUserID(_a0 context.Context, _a1 int64) (int64, error) {
16 | ret := _m.Called(_a0, _a1)
17 |
18 | var r0 int64
19 | if rf, ok := ret.Get(0).(func(context.Context, int64) int64); ok {
20 | r0 = rf(_a0, _a1)
21 | } else {
22 | r0 = ret.Get(0).(int64)
23 | }
24 |
25 | var r1 error
26 | if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
27 | r1 = rf(_a0, _a1)
28 | } else {
29 | r1 = ret.Error(1)
30 | }
31 |
32 | return r0, r1
33 | }
34 |
35 | // Insert provides a mock function with given fields: _a0, _a1
36 | func (_m *UserPermissionsProvider) Insert(_a0 context.Context, _a1 *model.UserPermissionsEntity) (*model.UserPermissionsEntity, error) {
37 | ret := _m.Called(_a0, _a1)
38 |
39 | var r0 *model.UserPermissionsEntity
40 | if rf, ok := ret.Get(0).(func(context.Context, *model.UserPermissionsEntity) *model.UserPermissionsEntity); ok {
41 | r0 = rf(_a0, _a1)
42 | } else {
43 | if ret.Get(0) != nil {
44 | r0 = ret.Get(0).(*model.UserPermissionsEntity)
45 | }
46 | }
47 |
48 | var r1 error
49 | if rf, ok := ret.Get(1).(func(context.Context, *model.UserPermissionsEntity) error); ok {
50 | r1 = rf(_a0, _a1)
51 | } else {
52 | r1 = ret.Error(1)
53 | }
54 |
55 | return r0, r1
56 | }
57 |
--------------------------------------------------------------------------------
/internal/charond/handler_list_user_permissions.go:
--------------------------------------------------------------------------------
1 | package charond
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/piotrkowalczuk/charon"
7 | charonrpc "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1"
8 | "github.com/piotrkowalczuk/charon/internal/grpcerr"
9 | "github.com/piotrkowalczuk/charon/internal/session"
10 | "google.golang.org/grpc/codes"
11 | )
12 |
13 | type listUserPermissionsHandler struct {
14 | *handler
15 | }
16 |
17 | func (luph *listUserPermissionsHandler) ListPermissions(ctx context.Context, req *charonrpc.ListUserPermissionsRequest) (*charonrpc.ListUserPermissionsResponse, error) {
18 | if req.Id <= 0 {
19 | return nil, grpcerr.E(codes.InvalidArgument, "missing user id")
20 | }
21 | act, err := luph.Actor(ctx)
22 | if err != nil {
23 | return nil, err
24 | }
25 | if err = luph.firewall(req, act); err != nil {
26 | return nil, err
27 | }
28 |
29 | permissions, err := luph.repository.permission.FindByUserID(ctx, req.Id)
30 | if err != nil {
31 | return nil, grpcerr.E(codes.Internal, "find permissions by user id query failed", err)
32 | }
33 |
34 | perms := make([]string, 0, len(permissions))
35 | for _, p := range permissions {
36 | perms = append(perms, p.Permission().String())
37 | }
38 |
39 | return &charonrpc.ListUserPermissionsResponse{
40 | Permissions: perms,
41 | }, nil
42 | }
43 |
44 | func (luph *listUserPermissionsHandler) firewall(req *charonrpc.ListUserPermissionsRequest, act *session.Actor) error {
45 | if act.User.IsSuperuser {
46 | return nil
47 | }
48 | if act.User.ID == req.Id {
49 | return nil
50 | }
51 | if act.Permissions.Contains(charon.UserPermissionCanRetrieve) {
52 | return nil
53 | }
54 |
55 | return grpcerr.E(codes.PermissionDenied, "list of user permissions cannot be retrieved, missing permission")
56 | }
57 |
--------------------------------------------------------------------------------
/internal/model/refresh_token.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "context"
5 | "database/sql"
6 | )
7 |
8 | // RefreshTokenProvider ...
9 | type RefreshTokenProvider interface {
10 | // Find ...
11 | Find(context.Context, *RefreshTokenFindExpr) ([]*RefreshTokenEntity, error)
12 | // FindOneByToken ...
13 | FindOneByToken(context.Context, string) (*RefreshTokenEntity, error)
14 | // Create ...
15 | Create(context.Context, *RefreshTokenEntity) (*RefreshTokenEntity, error)
16 | // UpdateOneByToken ...
17 | UpdateOneByToken(context.Context, string, *RefreshTokenPatch) (*RefreshTokenEntity, error)
18 | // FindOneByTokenAndUserID .
19 | FindOneByTokenAndUserID(ctx context.Context, token string, userID int64) (*RefreshTokenEntity, error)
20 | }
21 |
22 | // RefreshTokenRepository extends RefreshTokenRepositoryBase
23 | type RefreshTokenRepository struct {
24 | RefreshTokenRepositoryBase
25 | }
26 |
27 | // NewRefreshTokenRepository ...
28 | func NewRefreshTokenRepository(dbPool *sql.DB) RefreshTokenProvider {
29 | return &RefreshTokenRepository{
30 | RefreshTokenRepositoryBase: RefreshTokenRepositoryBase{
31 | DB: dbPool,
32 | Table: TableRefreshToken,
33 | Columns: TableRefreshTokenColumns,
34 | },
35 | }
36 | }
37 |
38 | // Create ...
39 | func (rtr *RefreshTokenRepository) Create(ctx context.Context, ent *RefreshTokenEntity) (*RefreshTokenEntity, error) {
40 | return rtr.Insert(ctx, ent)
41 | }
42 |
43 | // FindOneByTokenAndUserID ...
44 | func (rtr *RefreshTokenRepository) FindOneByTokenAndUserID(ctx context.Context, token string, userID int64) (*RefreshTokenEntity, error) {
45 | ent, err := rtr.FindOneByToken(ctx, token)
46 | if err != nil {
47 | return nil, err
48 | }
49 | if ent.UserID != userID {
50 | return nil, sql.ErrNoRows
51 | }
52 | return ent, nil
53 | }
54 |
--------------------------------------------------------------------------------
/internal/charond/handler_logout_test.go:
--------------------------------------------------------------------------------
1 | package charond
2 |
3 | import (
4 | "context"
5 | "testing"
6 |
7 | charonrpc "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1"
8 | "google.golang.org/grpc/codes"
9 | "google.golang.org/grpc/status"
10 | )
11 |
12 | func TestLogoutHandler_Logout(t *testing.T) {
13 | suite := &endToEndSuite{}
14 | suite.setup(t)
15 | defer suite.teardown(t)
16 |
17 | tkn, err := suite.charon.auth.Login(context.TODO(), &charonrpc.LoginRequest{
18 | Username: "test",
19 | Password: "test",
20 | })
21 | if err != nil {
22 | t.Fatalf("unexpected login error: %s: with code %s", status.Convert(err).Message(), status.Code(err))
23 | }
24 |
25 | if _, err := suite.charon.auth.Logout(context.TODO(), &charonrpc.LogoutRequest{
26 | AccessToken: tkn.Value,
27 | }); err != nil {
28 | t.Errorf("logout failure: %s", err.Error())
29 | }
30 | ok, err := suite.charon.auth.IsAuthenticated(context.TODO(), &charonrpc.IsAuthenticatedRequest{
31 | AccessToken: tkn.Value,
32 | })
33 | if err != nil {
34 | t.Fatalf("unexpected is authenticated error: %s: with code %s", status.Convert(err).Message(), status.Code(err))
35 | }
36 | if ok.Value {
37 | t.Errorf("user should not be authenticated")
38 | }
39 | }
40 |
41 | func TestLogoutHandler_Logout_missingToken(t *testing.T) {
42 | suite := &endToEndSuite{}
43 | suite.setup(t)
44 | defer suite.teardown(t)
45 |
46 | _, err := suite.charon.auth.Logout(context.TODO(), &charonrpc.LogoutRequest{})
47 | if err == nil {
48 | t.Fatal("error should not be nil")
49 | }
50 | if st, ok := status.FromError(err); ok {
51 | if st.Code() != codes.InvalidArgument {
52 | t.Errorf("wrong status code, expected %s but got %s", codes.InvalidArgument.String(), st.Code().String())
53 | }
54 | } else {
55 | t.Error("wrong error type")
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/data/test-selfsigned.crt:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIE6zCCA9OgAwIBAgIJAKst8YkJ6qNBMA0GCSqGSIb3DQEBCwUAMIGpMQswCQYD
3 | VQQGEwJQTDEVMBMGA1UECBQMRG9sbnnFm2zEhXNrMRMwEQYDVQQHFApXYcWCYnJ6
4 | eWNoMRQwEgYDVQQKEwtDaGFyb24gSW5jLjEUMBIGA1UECxMLT3BlbiBTb3VyY2Ux
5 | FzAVBgNVBAMTDnRlc3QubG9jYWwudGxkMSkwJwYJKoZIhvcNAQkBFhpwLmtvd2Fs
6 | Y3p1ay5wcml2QGdtYWlsLmNvbTAeFw0xNzA3MDkxMjM3NTBaFw0yNzA3MDcxMjM3
7 | NTBaMIGpMQswCQYDVQQGEwJQTDEVMBMGA1UECBQMRG9sbnnFm2zEhXNrMRMwEQYD
8 | VQQHFApXYcWCYnJ6eWNoMRQwEgYDVQQKEwtDaGFyb24gSW5jLjEUMBIGA1UECxML
9 | T3BlbiBTb3VyY2UxFzAVBgNVBAMTDnRlc3QubG9jYWwudGxkMSkwJwYJKoZIhvcN
10 | AQkBFhpwLmtvd2FsY3p1ay5wcml2QGdtYWlsLmNvbTCCASIwDQYJKoZIhvcNAQEB
11 | BQADggEPADCCAQoCggEBAO0pJa90MFIbwHrWAEjU4qecO3S/sW/lWL+BKwCThluL
12 | hq7p9ZE20fSVc9C94Ed0rOIbTXDwrp80nllsLzvNvhZ1Bi9bs0Qch1mlzA6PCGBD
13 | 6QI5fdazGlfuH70BqoXFMivjCqBVW9BNcXHftQm41nq1fdSKk2udCwpkI9bMEH4q
14 | xBmnD9fSUzZJu+EHKlStb2DwFRbMtJ4NpDAVf/+drOcDoI9j6Q3GiAnHMukCLl4w
15 | JDt7vUGQ4WouG6LfpQYz+Y/9BffC2mCmh5lZqb++k8a5gR0OvwB0h0PqyPfCtTY/
16 | klJiffM3ff0Mub8qrceGkmEzqjLL1dZfVqRQLDnmvsUCAwEAAaOCARIwggEOMB0G
17 | A1UdDgQWBBTwx64SsuKAoojTBND16m4uhzQ0/jCB3gYDVR0jBIHWMIHTgBTwx64S
18 | suKAoojTBND16m4uhzQ0/qGBr6SBrDCBqTELMAkGA1UEBhMCUEwxFTATBgNVBAgU
19 | DERvbG55xZtsxIVzazETMBEGA1UEBxQKV2HFgmJyenljaDEUMBIGA1UEChMLQ2hh
20 | cm9uIEluYy4xFDASBgNVBAsTC09wZW4gU291cmNlMRcwFQYDVQQDEw50ZXN0Lmxv
21 | Y2FsLnRsZDEpMCcGCSqGSIb3DQEJARYacC5rb3dhbGN6dWsucHJpdkBnbWFpbC5j
22 | b22CCQCrLfGJCeqjQTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQDs
23 | g029gFSZyJ4g5BHsSOEXZ2Nc5vRzrZWu/I9Ci5ztMAM/EzE5WIeBIVna1cfrLwL8
24 | qIwqiqkjrXiH6aEF1zH/UZVL8jen+s9y7Q1WOwvGqIAJQlbUEsMBa4vrmP203hVI
25 | oYdFlEi1+tfRDxQM9e86To4l/hAjvyjQuj31v45pUETZ/bVt+R4KUGaxVf3ETw36
26 | BwcX6KWDKVCs+ie0k0c1SppfGCxDFDQdpFwKxd/9RcJ2Oc6zcnb1i1577UVjYoES
27 | T+M/Ayyho6mGACaFwlBHmPHmfVJf6hj09pt6Z1938oGm19zj/ZKRSl1GtrST72p0
28 | KuvB5qQWEM5mhh1oZUzT
29 | -----END CERTIFICATE-----
30 |
--------------------------------------------------------------------------------
/internal/model/modelmock/permission_registry.go:
--------------------------------------------------------------------------------
1 | // Code generated by mockery v1.0.0. DO NOT EDIT.
2 |
3 | package modelmock
4 |
5 | import charon "github.com/piotrkowalczuk/charon"
6 | import context "context"
7 | import mock "github.com/stretchr/testify/mock"
8 |
9 | // PermissionRegistry is an autogenerated mock type for the PermissionRegistry type
10 | type PermissionRegistry struct {
11 | mock.Mock
12 | }
13 |
14 | // Exists provides a mock function with given fields: ctx, permission
15 | func (_m *PermissionRegistry) Exists(ctx context.Context, permission charon.Permission) bool {
16 | ret := _m.Called(ctx, permission)
17 |
18 | var r0 bool
19 | if rf, ok := ret.Get(0).(func(context.Context, charon.Permission) bool); ok {
20 | r0 = rf(ctx, permission)
21 | } else {
22 | r0 = ret.Get(0).(bool)
23 | }
24 |
25 | return r0
26 | }
27 |
28 | // Register provides a mock function with given fields: ctx, permissions
29 | func (_m *PermissionRegistry) Register(ctx context.Context, permissions charon.Permissions) (int64, int64, int64, error) {
30 | ret := _m.Called(ctx, permissions)
31 |
32 | var r0 int64
33 | if rf, ok := ret.Get(0).(func(context.Context, charon.Permissions) int64); ok {
34 | r0 = rf(ctx, permissions)
35 | } else {
36 | r0 = ret.Get(0).(int64)
37 | }
38 |
39 | var r1 int64
40 | if rf, ok := ret.Get(1).(func(context.Context, charon.Permissions) int64); ok {
41 | r1 = rf(ctx, permissions)
42 | } else {
43 | r1 = ret.Get(1).(int64)
44 | }
45 |
46 | var r2 int64
47 | if rf, ok := ret.Get(2).(func(context.Context, charon.Permissions) int64); ok {
48 | r2 = rf(ctx, permissions)
49 | } else {
50 | r2 = ret.Get(2).(int64)
51 | }
52 |
53 | var r3 error
54 | if rf, ok := ret.Get(3).(func(context.Context, charon.Permissions) error); ok {
55 | r3 = rf(ctx, permissions)
56 | } else {
57 | r3 = ret.Error(3)
58 | }
59 |
60 | return r0, r1, r2, r3
61 | }
62 |
--------------------------------------------------------------------------------
/cmd/charond/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 |
7 | _ "github.com/lib/pq"
8 | "github.com/piotrkowalczuk/charon/internal/charond"
9 | "github.com/piotrkowalczuk/charon/internal/service/logger"
10 | "go.uber.org/zap"
11 | )
12 |
13 | var (
14 | config configuration
15 | service = "charond"
16 | )
17 |
18 | func init() {
19 | config.init()
20 | }
21 |
22 | func main() {
23 | config.parse()
24 |
25 | log, err := logger.Init(service, version, logger.Opts{
26 | Environment: config.logger.environment,
27 | Level: config.logger.level,
28 | })
29 | if err != nil {
30 | fmt.Println(err)
31 | os.Exit(1)
32 | }
33 | rpcListener := initListener(log, config.host, config.port)
34 | debugListener := initListener(log, config.host, config.port+1)
35 |
36 | // TODO: update and make it optional
37 | //grpclog.SetLogger(sklog.NewGRPCLogger(logger))
38 |
39 | daemon := charond.NewDaemon(charond.DaemonOpts{
40 | Test: config.test,
41 | TLS: config.tls.enabled,
42 | TLSCertFile: config.tls.certFile,
43 | TLSKeyFile: config.tls.keyFile,
44 | Monitoring: config.monitoring.enabled,
45 | PostgresAddress: config.postgres.address + "&application_name=charond_" + version,
46 | PostgresDebug: config.postgres.debug,
47 | PasswordBCryptCost: config.password.bcrypt.cost,
48 | MnemosyneAddress: config.mnemosyned.address,
49 | MnemosyneTLS: config.mnemosyned.tls.enabled,
50 | MnemosyneTLSCertFile: config.mnemosyned.tls.certFile,
51 | Logger: log.Named("daemon"),
52 | RPCListener: rpcListener,
53 | DebugListener: debugListener,
54 | })
55 |
56 | if err := daemon.Run(); err != nil {
57 | log.Fatal("daemon returned an error", zap.Error(err))
58 | }
59 | defer daemon.Close()
60 |
61 | done := make(chan struct{})
62 | <-done
63 | }
64 |
--------------------------------------------------------------------------------
/internal/charond/handler_list_groups.go:
--------------------------------------------------------------------------------
1 | package charond
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/piotrkowalczuk/charon"
7 | charonrpc "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1"
8 | "github.com/piotrkowalczuk/charon/internal/grpcerr"
9 | "github.com/piotrkowalczuk/charon/internal/mapping"
10 | "github.com/piotrkowalczuk/charon/internal/model"
11 | "github.com/piotrkowalczuk/charon/internal/session"
12 |
13 | "google.golang.org/grpc/codes"
14 | )
15 |
16 | type listGroupsHandler struct {
17 | *handler
18 | }
19 |
20 | func (lgh *listGroupsHandler) List(ctx context.Context, req *charonrpc.ListGroupsRequest) (*charonrpc.ListGroupsResponse, error) {
21 | act, err := lgh.Actor(ctx)
22 | if err != nil {
23 | return nil, err
24 | }
25 | if err = lgh.firewall(req, act); err != nil {
26 | return nil, err
27 | }
28 |
29 | ents, err := lgh.repository.group.Find(ctx, &model.GroupFindExpr{
30 | Limit: req.GetLimit().Int64Or(10),
31 | Offset: req.GetOffset().Int64Or(0),
32 | OrderBy: mapping.OrderBy(req.GetOrderBy()),
33 | })
34 | if err != nil {
35 | return nil, grpcerr.E(codes.Internal, "find group query failed", err)
36 | }
37 |
38 | return lgh.response(ents)
39 | }
40 |
41 | func (lgh *listGroupsHandler) firewall(req *charonrpc.ListGroupsRequest, act *session.Actor) error {
42 | if act.User.IsSuperuser {
43 | return nil
44 | }
45 | if act.Permissions.Contains(charon.GroupCanRetrieve) {
46 | return nil
47 | }
48 |
49 | return grpcerr.E(codes.PermissionDenied, "list of groups cannot be retrieved, missing permission")
50 | }
51 |
52 | func (lgh *listGroupsHandler) response(ents []*model.GroupEntity) (*charonrpc.ListGroupsResponse, error) {
53 | msg, err := mapping.ReverseGroups(ents)
54 | if err != nil {
55 | return nil, grpcerr.E(codes.Internal, "group entities mapping failure", err)
56 |
57 | }
58 |
59 | return &charonrpc.ListGroupsResponse{Groups: msg}, nil
60 | }
61 |
--------------------------------------------------------------------------------
/internal/charond/handler_get_group.go:
--------------------------------------------------------------------------------
1 | package charond
2 |
3 | import (
4 | "context"
5 | "database/sql"
6 |
7 | "github.com/piotrkowalczuk/charon"
8 | charonrpc "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1"
9 | "github.com/piotrkowalczuk/charon/internal/grpcerr"
10 | "github.com/piotrkowalczuk/charon/internal/mapping"
11 | "github.com/piotrkowalczuk/charon/internal/model"
12 | "github.com/piotrkowalczuk/charon/internal/session"
13 | "google.golang.org/grpc/codes"
14 | )
15 |
16 | type getGroupHandler struct {
17 | *handler
18 | }
19 |
20 | func (ggh *getGroupHandler) Get(ctx context.Context, req *charonrpc.GetGroupRequest) (*charonrpc.GetGroupResponse, error) {
21 | if req.Id <= 0 {
22 | return nil, grpcerr.E(codes.InvalidArgument, "missing group id")
23 | }
24 | act, err := ggh.Actor(ctx)
25 | if err != nil {
26 | return nil, err
27 | }
28 | if err = ggh.firewall(req, act); err != nil {
29 | return nil, err
30 | }
31 |
32 | ent, err := ggh.repository.group.FindOneByID(ctx, req.Id)
33 | if err != nil {
34 | if err == sql.ErrNoRows {
35 | return nil, grpcerr.E(codes.NotFound, "group does not exists")
36 | }
37 | return nil, grpcerr.E(codes.Internal, "group cannot be fetched", err)
38 | }
39 |
40 | return ggh.response(ent)
41 | }
42 |
43 | func (ggh *getGroupHandler) firewall(req *charonrpc.GetGroupRequest, act *session.Actor) error {
44 | if act.User.IsSuperuser {
45 | return nil
46 | }
47 | if act.Permissions.Contains(charon.GroupCanRetrieve) {
48 | return nil
49 | }
50 |
51 | return grpcerr.E(codes.PermissionDenied, "group cannot be retrieved, missing permission")
52 | }
53 |
54 | func (ggh *getGroupHandler) response(ent *model.GroupEntity) (*charonrpc.GetGroupResponse, error) {
55 | msg, err := mapping.ReverseGroup(ent)
56 | if err != nil {
57 | return nil, grpcerr.E(codes.Internal, "group entity mapping failure", err)
58 | }
59 | return &charonrpc.GetGroupResponse{
60 | Group: msg,
61 | }, nil
62 | }
63 |
--------------------------------------------------------------------------------
/internal/charond/handler_set_user_groups.go:
--------------------------------------------------------------------------------
1 | package charond
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/lib/pq"
7 | "github.com/piotrkowalczuk/charon"
8 | charonrpc "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1"
9 | "github.com/piotrkowalczuk/charon/internal/grpcerr"
10 | "github.com/piotrkowalczuk/charon/internal/model"
11 | "github.com/piotrkowalczuk/charon/internal/session"
12 |
13 | "google.golang.org/grpc/codes"
14 | )
15 |
16 | type setUserGroupsHandler struct {
17 | *handler
18 | }
19 |
20 | func (sugh *setUserGroupsHandler) SetGroups(ctx context.Context, req *charonrpc.SetUserGroupsRequest) (*charonrpc.SetUserGroupsResponse, error) {
21 | act, err := sugh.Actor(ctx)
22 | if err != nil {
23 | return nil, err
24 | }
25 |
26 | if err = sugh.firewall(req, act); err != nil {
27 | return nil, err
28 | }
29 |
30 | created, removed, err := sugh.repository.userGroups.Set(ctx, req.UserId, req.Groups)
31 | if err != nil {
32 | switch model.ErrorConstraint(err) {
33 | case model.TableUserGroupsConstraintGroupIDForeignKey:
34 | return nil, grpcerr.E(codes.NotFound, "%s: group does not exist", err.(*pq.Error).Detail)
35 | case model.TableUserGroupsConstraintUserIDForeignKey:
36 | return nil, grpcerr.E(codes.NotFound, "%s: user does not exist", err.(*pq.Error).Detail)
37 | default:
38 | return nil, err
39 | }
40 | }
41 |
42 | return &charonrpc.SetUserGroupsResponse{
43 | Created: created,
44 | Removed: removed,
45 | Untouched: untouched(int64(len(req.Groups)), created, removed),
46 | }, nil
47 | }
48 |
49 | func (sugh *setUserGroupsHandler) firewall(req *charonrpc.SetUserGroupsRequest, act *session.Actor) error {
50 | if act.User.IsSuperuser {
51 | return nil
52 | }
53 | if act.Permissions.Contains(charon.UserGroupCanCreate) && act.Permissions.Contains(charon.UserGroupCanDelete) {
54 | return nil
55 | }
56 |
57 | return grpcerr.E(codes.PermissionDenied, "user groups cannot be set, missing permission")
58 | }
59 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | SERVICE=charon
2 | VERSION=$(shell git describe --tags --always --dirty)
3 | ifeq ($(version),)
4 | TAG=${VERSION}
5 | else
6 | TAG=$(version)
7 | endif
8 |
9 | PACKAGE=github.com/piotrkowalczuk/charon
10 | PACKAGE_CMD_DAEMON=$(PACKAGE)/cmd/$(SERVICE)d
11 | PACKAGE_CMD_CONTROL=$(PACKAGE)/cmd/$(SERVICE)ctl
12 | PACKAGE_CMD_GENERATOR=$(PACKAGE)/cmd/$(SERVICE)g
13 |
14 |
15 | LDFLAGS = -X 'main.version=$(VERSION)'
16 |
17 | .PHONY: all version build install gen test cover get publish
18 |
19 | all: get install
20 |
21 | version:
22 | echo ${VERSION} > publish/VERSION.txt
23 |
24 | build:
25 | CGO_ENABLED=0 GOOS=linux go build -ldflags "${LDFLAGS}" -a -o bin/${SERVICE}g ${PACKAGE_CMD_GENERATOR}
26 | CGO_ENABLED=0 GOOS=linux go build -ldflags "${LDFLAGS}" -a -o bin/${SERVICE}d ${PACKAGE_CMD_DAEMON}
27 | CGO_ENABLED=0 GOOS=linux go build -ldflags "${LDFLAGS}" -a -o bin/${SERVICE}ctl ${PACKAGE_CMD_CONTROL}
28 |
29 | install:
30 | go install -ldflags "${LDFLAGS}" ${PACKAGE_CMD_GENERATOR}
31 | go install -ldflags "${LDFLAGS}" ${PACKAGE_CMD_DAEMON}
32 | go install -ldflags "${LDFLAGS}" ${PACKAGE_CMD_CONTROL}
33 |
34 | gen:
35 | #./scripts/generate.sh
36 | bash ./.circleci/scripts/generate.sh golang
37 |
38 | test:
39 | ./.circleci/scripts/test.sh
40 | go tool cover -func=cover.out | tail -n 1
41 |
42 | cover: test
43 | go tool cover -html=cover.out
44 |
45 | get:
46 | bash ./.circleci/scripts/get_tool.sh github.com/golang/protobuf/proto v1.2.0
47 | go get -u gotest.tools/gotestsum
48 | go get -u github.com/vektra/mockery/cmd/mockery
49 | go get -u github.com/golang/dep/cmd/dep
50 | dep ensure
51 |
52 | publish:
53 | docker build \
54 | --build-arg VCS_REF=${VCS_REF} \
55 | --build-arg BUILD_DATE=`date -u +"%Y-%m-%dT%H:%M:%SZ"` \
56 | -t piotrkowalczuk/${SERVICE}:${TAG} .
57 | docker push piotrkowalczuk/${SERVICE}:${TAG}
58 |
59 | setup-python:
60 | python3 -m venv venv
61 | source ./venv/bin/activate.fish
62 | pip install grpc_tools
63 | pip install grpcio-tools
--------------------------------------------------------------------------------
/internal/charond/handler_list_permissions.go:
--------------------------------------------------------------------------------
1 | package charond
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/piotrkowalczuk/charon"
7 | charonrpc "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1"
8 | "github.com/piotrkowalczuk/charon/internal/grpcerr"
9 | "github.com/piotrkowalczuk/charon/internal/mapping"
10 | "github.com/piotrkowalczuk/charon/internal/model"
11 | "github.com/piotrkowalczuk/charon/internal/session"
12 |
13 | "google.golang.org/grpc/codes"
14 | )
15 |
16 | type listPermissionsHandler struct {
17 | *handler
18 | }
19 |
20 | func (lph *listPermissionsHandler) List(ctx context.Context, req *charonrpc.ListPermissionsRequest) (*charonrpc.ListPermissionsResponse, error) {
21 | act, err := lph.Actor(ctx)
22 | if err != nil {
23 | return nil, err
24 | }
25 | if err = lph.firewall(req, act); err != nil {
26 | return nil, err
27 | }
28 |
29 | entities, err := lph.repository.permission.Find(ctx, &model.PermissionFindExpr{
30 | Offset: req.Offset.Int64Or(0),
31 | Limit: req.Limit.Int64Or(10),
32 | OrderBy: mapping.OrderBy(req.OrderBy),
33 | Where: &model.PermissionCriteria{
34 | Subsystem: req.Subsystem,
35 | Module: req.Module,
36 | Action: req.Action,
37 | },
38 | })
39 | if err != nil {
40 | return nil, grpcerr.E(codes.Internal, "find permission query failed", err)
41 | }
42 |
43 | permissions := make([]string, 0, len(entities))
44 | for _, e := range entities {
45 | permissions = append(permissions, e.Permission().String())
46 | }
47 | return &charonrpc.ListPermissionsResponse{
48 | Permissions: permissions,
49 | }, nil
50 | }
51 |
52 | func (lph *listPermissionsHandler) firewall(req *charonrpc.ListPermissionsRequest, act *session.Actor) error {
53 | if act.User.IsSuperuser {
54 | return nil
55 | }
56 | if act.Permissions.Contains(charon.PermissionCanRetrieve) {
57 | return nil
58 | }
59 |
60 | return grpcerr.E(codes.PermissionDenied, "list of permissions cannot be retrieved, missing permission")
61 | }
62 |
--------------------------------------------------------------------------------
/internal/model/group_test.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "context"
5 | "testing"
6 | )
7 |
8 | func TestGroupRepository_IsGranted(t *testing.T) {
9 | suite := &postgresSuite{}
10 | suite.setup(t)
11 | defer suite.teardown(t)
12 |
13 | for ur := range loadGroupFixtures(t, suite.repository.group, groupPermissionsTestFixtures) {
14 | for pr := range loadPermissionFixtures(t, suite.repository.permission, ur.given.Permissions) {
15 | add := []*GroupPermissionsEntity{{
16 | GroupID: ur.got.ID,
17 | PermissionSubsystem: pr.got.Subsystem,
18 | PermissionModule: pr.got.Module,
19 | PermissionAction: pr.got.Action,
20 | }}
21 | for range loadGroupPermissionsFixtures(t, suite.repository.groupPermissions, add) {
22 | exists, err := suite.repository.group.IsGranted(context.TODO(), ur.given.ID, pr.given.Permission())
23 |
24 | if err != nil {
25 | t.Errorf("group permission cannot be found, unexpected error: %s", err.Error())
26 | continue
27 | }
28 |
29 | if !exists {
30 | t.Errorf("group permission not found for group %d and permission %d", ur.given.ID, pr.given.ID)
31 | } else {
32 | t.Logf("group permission relationship exists for group %d and permission %d", ur.given.ID, pr.given.ID)
33 | }
34 | }
35 | }
36 | }
37 | }
38 |
39 | type groupFixtures struct {
40 | got, given GroupEntity
41 | }
42 |
43 | func loadGroupFixtures(t *testing.T, r GroupProvider, f []*GroupEntity) chan groupFixtures {
44 | data := make(chan groupFixtures, 1)
45 |
46 | go func() {
47 | for _, given := range f {
48 | entity, err := r.Insert(context.TODO(), given)
49 | if err != nil {
50 | t.Errorf("group cannot be created, unexpected error: %s", err.Error())
51 | continue
52 | } else {
53 | t.Logf("group has been created, got id %d", entity.ID)
54 | }
55 |
56 | data <- groupFixtures{
57 | got: *entity,
58 | given: *given,
59 | }
60 | }
61 |
62 | close(data)
63 | }()
64 |
65 | return data
66 | }
67 |
--------------------------------------------------------------------------------
/internal/charond/suite_test.go:
--------------------------------------------------------------------------------
1 | package charond
2 |
3 | import (
4 | "context"
5 | "flag"
6 | "os"
7 | "runtime/debug"
8 | "testing"
9 | "time"
10 |
11 | _ "github.com/lib/pq"
12 | "github.com/piotrkowalczuk/charon/internal/grpcerr"
13 | "google.golang.org/grpc/codes"
14 | "google.golang.org/grpc/status"
15 | )
16 |
17 | var (
18 | testPostgresAddress string
19 | )
20 |
21 | func TestMain(m *testing.M) {
22 | flag.StringVar(&testPostgresAddress, "postgres.address", getStringEnvOr("CHAROND_POSTGRES_ADDRESS", "postgres://localhost/test?sslmode=disable"), "")
23 | flag.Parse()
24 |
25 | os.Exit(m.Run())
26 | }
27 |
28 | func getStringEnvOr(env, or string) string {
29 | if v := os.Getenv(env); v != "" {
30 | return v
31 | }
32 | return or
33 | }
34 |
35 | func timeout(ctx context.Context) context.Context {
36 | // TODO: leak
37 | ctx, _ = context.WithTimeout(ctx, 5*time.Second)
38 | return ctx
39 | }
40 |
41 | func assertErrorCode(t *testing.T, err error, code codes.Code, msg string) {
42 | t.Helper()
43 |
44 | if err == nil {
45 | t.Fatal("expected error")
46 | }
47 | if st, ok := status.FromError(err); ok {
48 | if st.Code() != code {
49 | t.Fatalf("wrong error code, expected '%s' but got '%s' for error: %s", code, st.Code(), err.Error())
50 | }
51 | if st.Message() != msg {
52 | t.Fatalf("wrong error message, expected '%s' but got '%s' for error: %s", msg, st.Message(), err.Error())
53 | }
54 | } else {
55 | t.Fatalf("expected grpc error, got %T", err)
56 | }
57 | }
58 |
59 | func assertError(t *testing.T, e1, e2 error) {
60 | t.Helper()
61 |
62 | if e1 != nil {
63 | if !grpcerr.Match(e1, e2) {
64 | t.Fatalf("error do not match, got %v", e2)
65 | }
66 | } else if e2 != nil {
67 | t.Fatal(e2)
68 | }
69 | }
70 |
71 | func recoverTest(t *testing.T) {
72 | t.Helper()
73 |
74 | if err := recover(); err != nil {
75 | t.Error(err, string(debug.Stack()))
76 | }
77 | }
78 |
79 | func brokenDate() time.Time {
80 | return time.Date(1, 1, 0, 0, 0, 0, 0, time.UTC)
81 | }
82 |
--------------------------------------------------------------------------------
/internal/charond/handler_list_group_permissions.go:
--------------------------------------------------------------------------------
1 | package charond
2 |
3 | import (
4 | "context"
5 | "database/sql"
6 |
7 | "github.com/piotrkowalczuk/charon"
8 | charonrpc "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1"
9 | "github.com/piotrkowalczuk/charon/internal/grpcerr"
10 | "github.com/piotrkowalczuk/charon/internal/session"
11 | "go.uber.org/zap"
12 | "google.golang.org/grpc/codes"
13 | )
14 |
15 | type listGroupPermissionsHandler struct {
16 | *handler
17 | }
18 |
19 | func (lgph *listGroupPermissionsHandler) ListPermissions(ctx context.Context, req *charonrpc.ListGroupPermissionsRequest) (*charonrpc.ListGroupPermissionsResponse, error) {
20 | if req.Id <= 0 {
21 | return nil, grpcerr.E(codes.InvalidArgument, "missing group id")
22 | }
23 |
24 | act, err := lgph.Actor(ctx)
25 | if err != nil {
26 | return nil, err
27 | }
28 | if err = lgph.firewall(req, act); err != nil {
29 | return nil, err
30 | }
31 |
32 | permissions, err := lgph.repository.permission.FindByGroupID(ctx, req.Id)
33 | if err != nil {
34 | if err == sql.ErrNoRows {
35 | lgph.logger.Error("group permissions retrieved", zap.Int64("group_id", req.Id), zap.Int("count", len(permissions)))
36 |
37 | return &charonrpc.ListGroupPermissionsResponse{}, nil
38 | }
39 | return nil, grpcerr.E(codes.Internal, "find permissions by group id query failed", err)
40 | }
41 |
42 | perms := make([]string, 0, len(permissions))
43 | for _, p := range permissions {
44 | perms = append(perms, p.Permission().String())
45 | }
46 |
47 | return &charonrpc.ListGroupPermissionsResponse{
48 | Permissions: perms,
49 | }, nil
50 | }
51 |
52 | func (lgph *listGroupPermissionsHandler) firewall(req *charonrpc.ListGroupPermissionsRequest, act *session.Actor) error {
53 | if act.User.IsSuperuser {
54 | return nil
55 | }
56 | if act.Permissions.Contains(charon.GroupPermissionCanRetrieve) {
57 | return nil
58 | }
59 |
60 | return grpcerr.E(codes.PermissionDenied, "list of group permissions cannot be retrieved, missing permission")
61 | }
62 |
--------------------------------------------------------------------------------
/internal/charond/handler_create_group.go:
--------------------------------------------------------------------------------
1 | package charond
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/piotrkowalczuk/charon"
7 | charonrpc "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1"
8 | "github.com/piotrkowalczuk/charon/internal/grpcerr"
9 | "github.com/piotrkowalczuk/charon/internal/mapping"
10 | "github.com/piotrkowalczuk/charon/internal/model"
11 | "github.com/piotrkowalczuk/charon/internal/session"
12 | "google.golang.org/grpc/codes"
13 | )
14 |
15 | type createGroupHandler struct {
16 | *handler
17 | }
18 |
19 | func (cgh *createGroupHandler) Create(ctx context.Context, req *charonrpc.CreateGroupRequest) (*charonrpc.CreateGroupResponse, error) {
20 | if len(req.Name) < 3 {
21 | return nil, grpcerr.E(codes.InvalidArgument, "group name is required and needs to be at least 3 characters long")
22 | }
23 | act, err := cgh.Actor(ctx)
24 | if err != nil {
25 | return nil, err
26 | }
27 | if err = cgh.firewall(req, act); err != nil {
28 | return nil, err
29 | }
30 |
31 | ent, err := cgh.repository.group.Create(ctx, act.User.ID, req.Name, req.Description)
32 | if err != nil {
33 | switch model.ErrorConstraint(err) {
34 | case model.TableGroupConstraintNameUnique:
35 | return nil, grpcerr.E(codes.AlreadyExists, "group with given name already exists")
36 | default:
37 | return nil, grpcerr.E(codes.Internal, "group fetch failure", err)
38 | }
39 | }
40 |
41 | return cgh.response(ent)
42 | }
43 |
44 | func (cgh *createGroupHandler) firewall(req *charonrpc.CreateGroupRequest, act *session.Actor) error {
45 | if act.User.IsSuperuser {
46 | return nil
47 | }
48 | if act.Permissions.Contains(charon.GroupCanCreate) {
49 | return nil
50 | }
51 |
52 | return grpcerr.E(codes.PermissionDenied, "group cannot be created, missing permission")
53 | }
54 |
55 | func (cgh *createGroupHandler) response(ent *model.GroupEntity) (*charonrpc.CreateGroupResponse, error) {
56 | msg, err := mapping.ReverseGroup(ent)
57 | if err != nil {
58 | return nil, grpcerr.E(codes.Internal, "group entity mapping failure", err)
59 | }
60 | return &charonrpc.CreateGroupResponse{Group: msg}, nil
61 | }
62 |
--------------------------------------------------------------------------------
/internal/charond/database.go:
--------------------------------------------------------------------------------
1 | package charond
2 |
3 | import (
4 | "context"
5 | "database/sql"
6 |
7 | "github.com/piotrkowalczuk/charon/internal/model"
8 | "github.com/piotrkowalczuk/charon/internal/password"
9 | )
10 |
11 | type repositories struct {
12 | user model.UserProvider
13 | userGroups model.UserGroupsProvider
14 | userPermissions model.UserPermissionsProvider
15 | permission model.PermissionProvider
16 | group model.GroupProvider
17 | groupPermissions model.GroupPermissionsProvider
18 | refreshToken model.RefreshTokenProvider
19 | }
20 |
21 | func newRepositories(db *sql.DB) repositories {
22 | return repositories{
23 | user: model.NewUserRepository(db),
24 | userGroups: model.NewUserGroupsRepository(db),
25 | userPermissions: model.NewUserPermissionsRepository(db),
26 | permission: model.NewPermissionRepository(db),
27 | group: model.NewGroupRepository(db),
28 | groupPermissions: model.NewGroupPermissionsRepository(db),
29 | refreshToken: model.NewRefreshTokenRepository(db),
30 | }
31 | }
32 |
33 | func execQueries(db *sql.DB, queries ...string) (err error) {
34 | exec := func(query string) {
35 | if err != nil {
36 | return
37 | }
38 |
39 | _, err = db.Exec(query)
40 | }
41 |
42 | for _, q := range queries {
43 | exec(q)
44 | }
45 |
46 | return
47 | }
48 |
49 | func setupDatabase(db *sql.DB) error {
50 | return execQueries(
51 | db,
52 | model.SQL,
53 | )
54 | }
55 |
56 | func teardownDatabase(db *sql.DB) error {
57 | return execQueries(
58 | db,
59 | `DROP SCHEMA IF EXISTS charon CASCADE`,
60 | )
61 | }
62 |
63 | func createDummyTestUser(ctx context.Context, usrProvider model.UserProvider, rftProvider model.RefreshTokenProvider, hasher password.Hasher) error {
64 | pass, err := hasher.Hash([]byte("test"))
65 | if err != nil {
66 | return err
67 | }
68 | usr, err := usrProvider.CreateSuperuser(ctx, "test", pass, "Test", "Test")
69 | if err != nil {
70 | return err
71 | }
72 |
73 | _, err = rftProvider.Create(ctx, &model.RefreshTokenEntity{Token: "test", UserID: usr.ID})
74 | return err
75 | }
76 |
--------------------------------------------------------------------------------
/pb/rpc/charond/v1/auth.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package charon.rpc.charond.v1;
4 |
5 | option go_package = "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1;charond";
6 | option java_multiple_files = true;
7 | option java_package = "com.github.charon.rpc.charond.v1";
8 |
9 | import "google/protobuf/empty.proto";
10 | import "google/protobuf/wrappers.proto";
11 |
12 | service Auth {
13 | rpc Login (LoginRequest) returns (google.protobuf.StringValue);
14 | rpc Logout (LogoutRequest) returns (google.protobuf.Empty);
15 | rpc IsAuthenticated (IsAuthenticatedRequest) returns (google.protobuf.BoolValue);
16 | rpc Actor (google.protobuf.StringValue) returns (ActorResponse);
17 | rpc IsGranted (IsGrantedRequest) returns (google.protobuf.BoolValue);
18 | rpc BelongsTo (BelongsToRequest) returns (google.protobuf.BoolValue);
19 | }
20 |
21 | message LoginRequest {
22 | string username = 1 [deprecated=true];
23 | string password = 2 [deprecated=true];
24 | string client = 3;
25 | reserved 13 to 19;
26 | oneof strategy {
27 | UsernameAndPasswordStrategy username_and_password = 11;
28 | RefreshTokenStrategy refresh_token = 12;
29 | }
30 | }
31 |
32 | message LogoutRequest {
33 | string access_token = 1;
34 | }
35 |
36 | message IsAuthenticatedRequest {
37 | string access_token = 1;
38 | }
39 |
40 | message IsGrantedRequest {
41 | int64 user_id = 1;
42 | string permission = 2;
43 | }
44 |
45 | message BelongsToRequest {
46 | int64 user_id = 1;
47 | int64 group_id = 2;
48 | }
49 |
50 | message ActorResponse {
51 | int64 id = 1;
52 | string username = 2;
53 | string first_name = 3;
54 | string last_name = 4;
55 | repeated string permissions = 5;
56 | bool is_superuser = 6;
57 | bool is_active = 7;
58 | bool is_stuff = 8 [deprecated = true];
59 | bool is_confirmed = 9;
60 | bool is_staff = 10;
61 | }
62 |
63 | message UsernameAndPasswordStrategy {
64 | string username = 1;
65 | string password = 2;
66 | }
67 |
68 | message RefreshTokenStrategy {
69 | string refresh_token = 1;
70 | }
71 |
--------------------------------------------------------------------------------
/internal/charond/handler_delete_group.go:
--------------------------------------------------------------------------------
1 | package charond
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/golang/protobuf/ptypes/wrappers"
7 | "github.com/piotrkowalczuk/charon"
8 | charonrpc "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1"
9 | "github.com/piotrkowalczuk/charon/internal/grpcerr"
10 | "github.com/piotrkowalczuk/charon/internal/model"
11 | "github.com/piotrkowalczuk/charon/internal/session"
12 | "google.golang.org/grpc/codes"
13 | )
14 |
15 | type deleteGroupHandler struct {
16 | *handler
17 | }
18 |
19 | func (dgh *deleteGroupHandler) Delete(ctx context.Context, req *charonrpc.DeleteGroupRequest) (*wrappers.BoolValue, error) {
20 | if req.Id <= 0 {
21 | return nil, grpcerr.E(codes.InvalidArgument, "group cannot be deleted, invalid id")
22 | }
23 |
24 | act, err := dgh.Actor(ctx)
25 | if err != nil {
26 | return nil, err
27 | }
28 | if err = dgh.firewall(req, act); err != nil {
29 | return nil, err
30 | }
31 |
32 | aff, err := dgh.repository.group.DeleteOneByID(ctx, req.Id)
33 | if err != nil {
34 | //if err == sql.ErrNoRows {
35 | // return nil, grpcerr.E(codes.NotFound, "group deletion failure", err)
36 | //}
37 | switch model.ErrorConstraint(err) {
38 | case model.TableUserGroupsConstraintGroupIDForeignKey:
39 | return nil, grpcerr.E(codes.FailedPrecondition, "group cannot be removed, users are assigned to it")
40 | case model.TableGroupPermissionsConstraintGroupIDForeignKey:
41 | return nil, grpcerr.E(codes.FailedPrecondition, "group cannot be removed, permissions are assigned to it")
42 | default:
43 | return nil, grpcerr.E(codes.Internal, "group deletion failure", err)
44 | }
45 | }
46 |
47 | if aff == 0 {
48 | return nil, grpcerr.E(codes.NotFound, "group cannot be removed, does not exists")
49 | }
50 |
51 | return &wrappers.BoolValue{
52 | Value: aff > 0,
53 | }, nil
54 | }
55 |
56 | func (dgh *deleteGroupHandler) firewall(req *charonrpc.DeleteGroupRequest, act *session.Actor) error {
57 | if act.User.IsSuperuser {
58 | return nil
59 | }
60 | if act.Permissions.Contains(charon.GroupCanDelete) {
61 | return nil
62 | }
63 |
64 | return grpcerr.E(codes.PermissionDenied, "group cannot be removed, missing permission")
65 | }
66 |
--------------------------------------------------------------------------------
/internal/model/user_groups.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "context"
5 | "database/sql"
6 | "fmt"
7 | )
8 |
9 | // UserGroupsProvider ...
10 | type UserGroupsProvider interface {
11 | Insert(ctx context.Context, ent *UserGroupsEntity) (*UserGroupsEntity, error)
12 | Exists(ctx context.Context, userID, groupID int64) (bool, error)
13 | Find(ctx context.Context, expr *UserGroupsFindExpr) ([]*UserGroupsEntity, error)
14 | Set(ctx context.Context, userID int64, groupIDs []int64) (int64, int64, error)
15 | DeleteByUserID(ctx context.Context, id int64) (int64, error)
16 | }
17 |
18 | // UserGroupsRepository ...
19 | type UserGroupsRepository struct {
20 | UserGroupsRepositoryBase
21 | deleteByUserIDQuery string
22 | }
23 |
24 | // NewUserGroupsRepository ...
25 | func NewUserGroupsRepository(dbPool *sql.DB) UserGroupsProvider {
26 | return &UserGroupsRepository{
27 | UserGroupsRepositoryBase: UserGroupsRepositoryBase{
28 | DB: dbPool,
29 | Table: TableUserGroups,
30 | Columns: TableUserGroupsColumns,
31 | },
32 | deleteByUserIDQuery: fmt.Sprintf("DELETE FROM %s WHERE %s = $1", TableUserGroups, TableUserGroupsColumnUserID),
33 | }
34 | }
35 |
36 | // Exists implements UserGroupsProvider interface.
37 | func (ugr *UserGroupsRepository) Exists(ctx context.Context, userID, groupID int64) (bool, error) {
38 | var exists bool
39 | if err := ugr.DB.QueryRowContext(ctx, existsManyToManyQuery(ugr.Table, TableUserGroupsColumnUserID, TableUserGroupsColumnGroupID), userID, groupID).Scan(&exists); err != nil {
40 | return false, err
41 | }
42 |
43 | return exists, nil
44 | }
45 |
46 | // Set implements UserGroupsProvider interface.
47 | func (ugr *UserGroupsRepository) Set(ctx context.Context, userID int64, groupIDs []int64) (int64, int64, error) {
48 | return setManyToMany(ugr.DB, ctx, ugr.Table, TableUserGroupsColumnUserID, TableUserGroupsColumnGroupID, userID, groupIDs)
49 | }
50 |
51 | // DeleteByUserID removes user from all groups he belongs to.
52 | func (ugr *UserGroupsRepository) DeleteByUserID(ctx context.Context, id int64) (int64, error) {
53 | res, err := ugr.DB.ExecContext(ctx, ugr.deleteByUserIDQuery, id)
54 | if err != nil {
55 | return 0, err
56 | }
57 | return res.RowsAffected()
58 | }
59 |
--------------------------------------------------------------------------------
/internal/charond/handler_list_refresh_tokens.go:
--------------------------------------------------------------------------------
1 | package charond
2 |
3 | import (
4 | "github.com/piotrkowalczuk/charon"
5 | charonrpc "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1"
6 | "github.com/piotrkowalczuk/charon/internal/grpcerr"
7 | "github.com/piotrkowalczuk/charon/internal/mapping"
8 | "github.com/piotrkowalczuk/charon/internal/model"
9 | "github.com/piotrkowalczuk/charon/internal/session"
10 | "github.com/piotrkowalczuk/qtypes"
11 | "golang.org/x/net/context"
12 | "google.golang.org/grpc/codes"
13 | )
14 |
15 | type listRefreshTokensHandler struct {
16 | *handler
17 | }
18 |
19 | func (lrth *listRefreshTokensHandler) List(ctx context.Context, req *charonrpc.ListRefreshTokensRequest) (*charonrpc.ListRefreshTokensResponse, error) {
20 | act, err := lrth.Actor(ctx)
21 | if err != nil {
22 | return nil, err
23 | }
24 | if err = lrth.firewall(req, act); err != nil {
25 | return nil, err
26 | }
27 |
28 | ents, err := lrth.repository.refreshToken.Find(ctx, &model.RefreshTokenFindExpr{
29 | Limit: req.GetLimit().Int64Or(10),
30 | Offset: req.GetOffset().Int64Or(0),
31 | OrderBy: mapping.OrderBy(req.GetOrderBy()),
32 | Where: mapping.RefreshTokenQuery(req.GetQuery()),
33 | })
34 | if err != nil {
35 | return nil, grpcerr.E(codes.Internal, "find refresh token query failed", err)
36 | }
37 |
38 | msg, err := mapping.ReverseRefreshTokens(ents)
39 | if err != nil {
40 | return nil, grpcerr.E(codes.Internal, "refresh token reverse mapping failure")
41 | }
42 | return &charonrpc.ListRefreshTokensResponse{
43 | RefreshTokens: msg,
44 | }, nil
45 | }
46 |
47 | func (lrth *listRefreshTokensHandler) firewall(req *charonrpc.ListRefreshTokensRequest, act *session.Actor) error {
48 | if act.User.IsSuperuser {
49 | return nil
50 | }
51 | if act.Permissions.Contains(charon.RefreshTokenCanRetrieveAsStranger) {
52 | return nil
53 | }
54 | if act.Permissions.Contains(charon.RefreshTokenCanRetrieveAsOwner) {
55 | if req.Query == nil {
56 | req.Query = &charonrpc.RefreshTokenQuery{}
57 | }
58 | req.Query.UserId = qtypes.EqualInt64(act.User.ID)
59 | return nil
60 | }
61 |
62 | return grpcerr.E(codes.PermissionDenied, "list of refresh tokens cannot be retrieved, missing permission")
63 | }
64 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Charon [](https://circleci.com/gh/piotrkowalczuk/charon/tree/master)
2 |
3 | [](http://godoc.org/github.com/piotrkowalczuk/charon)
4 | [](https://codeclimate.com/github/piotrkowalczuk/charon/test_coverage)
5 | [](https://codeclimate.com/github/piotrkowalczuk/charon/maintainability)
6 | [](https://hub.docker.com/r/piotrkowalczuk/charon/)
7 | [](https://pypi.python.org/pypi/charon-client)
8 |
9 |
10 |
11 | ## Quick Start
12 |
13 | ### Installation
14 |
15 | ```bash
16 | $ go install github.com/piotrkowalczuk/charon/cmd/charond
17 | $ go install github.com/piotrkowalczuk/charon/cmd/charonctl
18 | ```
19 |
20 | ### Superuser
21 |
22 | ```bash
23 | $ charonctl register -address=localhost:8080 -auth.disabled -register.superuser=true -register.username="j.snow@gmail.com" -register.password=123 -register.firstname=John -register.lastname=Snow
24 | ```
25 | ## Example
26 |
27 | TODO
28 |
29 | ## Contribution
30 |
31 | @TODO
32 |
33 | ### Documentation
34 |
35 | @TODO
36 |
37 | ### TODO
38 | - [x] Auth
39 | - [x] login
40 | - [x] logout
41 | - [x] is authenticated
42 | - [x] subject
43 | - [x] is granted
44 | - [x] belongs to
45 | - [x] Permission
46 | - [x] get
47 | - [x] list
48 | - [x] register
49 | - [x] Group
50 | - [x] get
51 | - [x] list
52 | - [x] modify
53 | - [x] delete
54 | - [x] create
55 | - [x] set permissions
56 | - [x] list permissions
57 | - [x] User
58 | - [x] get
59 | - [x] list
60 | - [x] modify
61 | - [x] delete
62 | - [x] create
63 | - [x] set permissions
64 | - [x] set groups
65 | - [x] list permissions
66 | - [x] list groups
67 | - [x] Refresh Token
68 | - [x] Create
69 | - [x] Revoke
70 | - [x] List
71 |
--------------------------------------------------------------------------------
/internal/charond/handler_set_user_permissions.go:
--------------------------------------------------------------------------------
1 | package charond
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/lib/pq"
7 | "github.com/piotrkowalczuk/charon"
8 | charonrpc "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1"
9 | "github.com/piotrkowalczuk/charon/internal/grpcerr"
10 | "github.com/piotrkowalczuk/charon/internal/model"
11 | "github.com/piotrkowalczuk/charon/internal/session"
12 |
13 | "google.golang.org/grpc/codes"
14 | )
15 |
16 | type setUserPermissionsHandler struct {
17 | *handler
18 | }
19 |
20 | func (suph *setUserPermissionsHandler) SetPermissions(ctx context.Context, req *charonrpc.SetUserPermissionsRequest) (*charonrpc.SetUserPermissionsResponse, error) {
21 | act, err := suph.Actor(ctx)
22 | if err != nil {
23 | return nil, err
24 | }
25 |
26 | if err = suph.firewall(req, act); err != nil {
27 | return nil, err
28 | }
29 |
30 | permissions := charon.NewPermissions(req.Permissions...)
31 | if req.Force {
32 | _, err := suph.repository.permission.InsertMissing(ctx, permissions)
33 | if err != nil {
34 | return nil, err
35 | }
36 | }
37 |
38 | created, removed, err := suph.repository.user.SetPermissions(ctx, req.UserId, permissions...)
39 | if err != nil {
40 | switch model.ErrorConstraint(err) {
41 | case model.TableUserPermissionsConstraintUserIDForeignKey:
42 | return nil, grpcerr.E(codes.NotFound, "%s: user does not exist", err.(*pq.Error).Detail)
43 | case model.TableUserPermissionsConstraintPermissionSubsystemPermissionModulePermissionActionForeignKey:
44 | return nil, grpcerr.E(codes.NotFound, "%s: permission does not exist", err.(*pq.Error).Detail)
45 | default:
46 | return nil, err
47 | }
48 | }
49 |
50 | return &charonrpc.SetUserPermissionsResponse{
51 | Created: created,
52 | Removed: removed,
53 | Untouched: untouched(int64(len(req.Permissions)), created, removed),
54 | }, nil
55 | }
56 |
57 | func (suph *setUserPermissionsHandler) firewall(req *charonrpc.SetUserPermissionsRequest, act *session.Actor) error {
58 | if act.User.IsSuperuser {
59 | return nil
60 | }
61 |
62 | if act.Permissions.Contains(charon.UserPermissionCanCreate) && act.Permissions.Contains(charon.UserPermissionCanDelete) {
63 | return nil
64 | }
65 |
66 | return grpcerr.E(codes.PermissionDenied, "user permissions cannot be set, missing permission")
67 | }
68 |
--------------------------------------------------------------------------------
/internal/charonctl/console_register_user.go:
--------------------------------------------------------------------------------
1 | package charonctl
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "os"
7 |
8 | "github.com/piotrkowalczuk/charon"
9 | charonrpc "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1"
10 | "github.com/piotrkowalczuk/ntypes"
11 | "google.golang.org/grpc/codes"
12 | "google.golang.org/grpc/status"
13 | )
14 |
15 | type RegisterUserArg struct {
16 | IfNotExists bool
17 | Username string
18 | Password string
19 | FirstName string
20 | LastName string
21 | Superuser bool
22 | Confirmed bool
23 | Staff bool
24 | Active bool
25 | Permissions charon.Permissions
26 | }
27 |
28 | type consoleRegisterUser struct {
29 | user charonrpc.UserManagerClient
30 | }
31 |
32 | func (cru *consoleRegisterUser) RegisterUser(ctx context.Context, arg *RegisterUserArg) error {
33 | res, err := cru.user.Create(ctx, &charonrpc.CreateUserRequest{
34 | Username: arg.Username,
35 | PlainPassword: arg.Password,
36 | FirstName: arg.FirstName,
37 | LastName: arg.LastName,
38 | IsSuperuser: &ntypes.Bool{Bool: arg.Superuser, Valid: true},
39 | IsConfirmed: &ntypes.Bool{Bool: arg.Confirmed, Valid: true},
40 | IsStaff: &ntypes.Bool{Bool: arg.Staff, Valid: true},
41 | IsActive: &ntypes.Bool{Bool: arg.Active, Valid: true},
42 | })
43 | if err != nil {
44 | if arg.IfNotExists && status.Code(err) == codes.AlreadyExists {
45 | return &Error{
46 | Msg: fmt.Sprintf("user already exists: %s\n", arg.Username),
47 | Err: err,
48 | }
49 | }
50 | return &Error{
51 | Msg: "registration failure",
52 | Err: err,
53 | }
54 | }
55 |
56 | if arg.Superuser {
57 | fmt.Printf("superuser has been created: %s\n", res.User.Username)
58 | } else {
59 | fmt.Printf("user has been created: %s\n", res.User.Username)
60 | }
61 |
62 | if len(arg.Permissions) > 0 {
63 | if arg.Superuser {
64 | // superuser does not need permissions
65 | os.Exit(0)
66 | }
67 |
68 | if _, err = cru.user.SetPermissions(ctx, &charonrpc.SetUserPermissionsRequest{
69 | UserId: res.User.Id,
70 | Permissions: arg.Permissions.Strings(),
71 | }); err != nil {
72 | return &Error{
73 | Msg: "permission assigment failure",
74 | Err: err,
75 | }
76 | }
77 |
78 | fmt.Println("users permissions has been set")
79 | }
80 | return nil
81 | }
82 |
--------------------------------------------------------------------------------
/internal/charond/handler_actor.go:
--------------------------------------------------------------------------------
1 | package charond
2 |
3 | import (
4 | "context"
5 | "database/sql"
6 |
7 | "github.com/golang/protobuf/ptypes/wrappers"
8 | charonrpc "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1"
9 | "github.com/piotrkowalczuk/charon/internal/grpcerr"
10 | "github.com/piotrkowalczuk/charon/internal/session"
11 | "github.com/piotrkowalczuk/mnemosyne/mnemosynerpc"
12 | "google.golang.org/grpc/codes"
13 | )
14 |
15 | type actorHandler struct {
16 | *handler
17 | }
18 |
19 | func (sh *actorHandler) Actor(ctx context.Context, r *wrappers.StringValue) (*charonrpc.ActorResponse, error) {
20 | var ses *mnemosynerpc.Session
21 |
22 | if r.Value == "" {
23 | res, err := sh.session.Context(ctx, none())
24 | if err != nil {
25 | return nil, handleMnemosyneError(err)
26 | }
27 | ses = res.Session
28 | } else {
29 | res, err := sh.session.Get(ctx, &mnemosynerpc.GetRequest{
30 | AccessToken: r.Value,
31 | })
32 | if err != nil {
33 | return nil, handleMnemosyneError(err)
34 | }
35 | ses = res.Session
36 | }
37 |
38 | id, err := session.ActorID(ses.SubjectId).UserID()
39 | if err != nil {
40 | return nil, grpcerr.E(codes.Internal, "invalid session actor id")
41 | }
42 |
43 | ent, err := sh.repository.user.FindOneByID(ctx, id)
44 | switch err {
45 | case nil:
46 | case sql.ErrNoRows:
47 | return nil, grpcerr.E(codes.NotFound, "actor does not exists for given id")
48 | default:
49 | return nil, grpcerr.E(codes.Internal, "actor retrieval failure", err)
50 | }
51 |
52 | permissionEntities, err := sh.repository.permission.FindByUserID(ctx, id)
53 | switch err {
54 | case nil, sql.ErrNoRows:
55 | default:
56 | return nil, grpcerr.E(codes.Internal, "actor list of permissions failure", err)
57 | }
58 |
59 | permissions := make([]string, 0, len(permissionEntities))
60 | for _, e := range permissionEntities {
61 | permissions = append(permissions, e.Permission().String())
62 | }
63 |
64 | return &charonrpc.ActorResponse{
65 | Id: int64(ent.ID),
66 | Username: ent.Username,
67 | FirstName: ent.FirstName,
68 | LastName: ent.LastName,
69 | Permissions: permissions,
70 | IsActive: ent.IsActive,
71 | IsConfirmed: ent.IsConfirmed,
72 | IsStuff: ent.IsStaff,
73 | IsStaff: ent.IsStaff,
74 | IsSuperuser: ent.IsSuperuser,
75 | }, nil
76 | }
77 |
--------------------------------------------------------------------------------
/internal/charond/handler_set_group_permissions.go:
--------------------------------------------------------------------------------
1 | package charond
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/lib/pq"
7 | "github.com/piotrkowalczuk/charon"
8 | charonrpc "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1"
9 | "github.com/piotrkowalczuk/charon/internal/grpcerr"
10 | "github.com/piotrkowalczuk/charon/internal/model"
11 | "github.com/piotrkowalczuk/charon/internal/session"
12 |
13 | "google.golang.org/grpc/codes"
14 | )
15 |
16 | type setGroupPermissionsHandler struct {
17 | *handler
18 | }
19 |
20 | func (sgph *setGroupPermissionsHandler) SetPermissions(ctx context.Context, req *charonrpc.SetGroupPermissionsRequest) (*charonrpc.SetGroupPermissionsResponse, error) {
21 | act, err := sgph.Actor(ctx)
22 | if err != nil {
23 | return nil, err
24 | }
25 |
26 | if err = sgph.firewall(req, act); err != nil {
27 | return nil, err
28 | }
29 |
30 | permissions := charon.NewPermissions(req.Permissions...)
31 | if req.Force {
32 | _, err := sgph.repository.permission.InsertMissing(ctx, permissions)
33 | if err != nil {
34 | return nil, err
35 | }
36 | }
37 |
38 | created, removed, err := sgph.repository.group.SetPermissions(ctx, req.GroupId, permissions...)
39 | if err != nil {
40 | switch model.ErrorConstraint(err) {
41 | case model.TableGroupPermissionsConstraintGroupIDForeignKey:
42 | return nil, grpcerr.E(codes.NotFound, "%s: group does not exist", err.(*pq.Error).Detail)
43 | case model.TableGroupPermissionsConstraintPermissionSubsystemPermissionModulePermissionActionForeignKey:
44 | return nil, grpcerr.E(codes.NotFound, "%s: permission does not exist", err.(*pq.Error).Detail)
45 | default:
46 | return nil, err
47 | }
48 | }
49 |
50 | return &charonrpc.SetGroupPermissionsResponse{
51 | Created: created,
52 | Removed: removed,
53 | Untouched: untouched(int64(len(req.Permissions)), created, removed),
54 | }, nil
55 | }
56 |
57 | func (sgph *setGroupPermissionsHandler) firewall(req *charonrpc.SetGroupPermissionsRequest, act *session.Actor) error {
58 | if act.User.IsSuperuser {
59 | return nil
60 | }
61 | if act.Permissions.Contains(charon.GroupPermissionCanCreate) && act.Permissions.Contains(charon.GroupPermissionCanDelete) {
62 | return nil
63 | }
64 |
65 | return grpcerr.E(codes.PermissionDenied, "group permissions cannot be set, missing permission")
66 | }
67 |
--------------------------------------------------------------------------------
/pb/rpc/charond/v1/refresh_token.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package charon.rpc.charond.v1;
4 |
5 | option go_package = "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1;charond";
6 | option java_multiple_files = true;
7 | option java_package = "com.github.charon.rpc.charond.v1";
8 |
9 | import "google/protobuf/timestamp.proto";
10 | import "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1/common.proto";
11 | import "qtypes/qtypes.proto";
12 | import "ntypes/ntypes.proto";
13 |
14 | service RefreshTokenManager {
15 | rpc Create(CreateRefreshTokenRequest) returns (CreateRefreshTokenResponse) {};
16 | rpc Revoke(RevokeRefreshTokenRequest) returns (RevokeRefreshTokenResponse) {};
17 | rpc List(ListRefreshTokensRequest) returns (ListRefreshTokensResponse) {};
18 | }
19 |
20 | message RefreshToken {
21 | string token = 1;
22 | ntypes.String notes = 2;
23 | int64 user_id = 3;
24 | bool revoked = 4;
25 | google.protobuf.Timestamp expire_at = 5;
26 | google.protobuf.Timestamp last_used_at = 6;
27 | google.protobuf.Timestamp created_at = 7;
28 | ntypes.Int64 created_by = 8;
29 | google.protobuf.Timestamp updated_at = 9;
30 | ntypes.Int64 updated_by = 10;
31 | }
32 |
33 | message RefreshTokenQuery {
34 | qtypes.Int64 user_id = 1;
35 | qtypes.String notes = 2;
36 | ntypes.Bool revoked = 3;
37 | qtypes.Timestamp expire_at = 4;
38 | qtypes.Timestamp last_used_at = 5;
39 | qtypes.Timestamp created_at = 6;
40 | qtypes.Timestamp updated_at = 7;
41 | }
42 |
43 | message CreateRefreshTokenRequest {
44 | ntypes.String notes = 1;
45 | google.protobuf.Timestamp expire_at = 2;
46 | }
47 |
48 | message CreateRefreshTokenResponse {
49 | RefreshToken refresh_token = 1;
50 | }
51 |
52 | message ListRefreshTokensRequest {
53 | ntypes.Int64 offset = 1;
54 | ntypes.Int64 limit = 2;
55 | repeated Order order_by = 3;
56 | reserved 4 to 10;
57 |
58 | RefreshTokenQuery query = 11;
59 | }
60 |
61 | message ListRefreshTokensResponse {
62 | repeated RefreshToken refresh_tokens = 1;
63 | }
64 |
65 | message RevokeRefreshTokenRequest {
66 | string token = 1;
67 | int64 user_id = 2;
68 | }
69 |
70 | message RevokeRefreshTokenResponse {
71 | RefreshToken refresh_token = 1;
72 | }
73 |
--------------------------------------------------------------------------------
/internal/mapping/refresh_token.go:
--------------------------------------------------------------------------------
1 | package mapping
2 |
3 | import (
4 | "github.com/golang/protobuf/ptypes"
5 | pbts "github.com/golang/protobuf/ptypes/timestamp"
6 | charonrpc "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1"
7 | "github.com/piotrkowalczuk/charon/internal/model"
8 | "github.com/piotrkowalczuk/ntypes"
9 | )
10 |
11 | func ReverseRefreshToken(ent *model.RefreshTokenEntity) (*charonrpc.RefreshToken, error) {
12 | var (
13 | err error
14 | expireAt, lastUsedAt, createdAt, updatedAt *pbts.Timestamp
15 | )
16 |
17 | if createdAt, err = ptypes.TimestampProto(ent.CreatedAt); err != nil {
18 | return nil, err
19 | }
20 | if ent.UpdatedAt.Valid {
21 | if updatedAt, err = ptypes.TimestampProto(ent.UpdatedAt.Time); err != nil {
22 | return nil, err
23 | }
24 | }
25 | if ent.ExpireAt.Valid {
26 | if expireAt, err = ptypes.TimestampProto(ent.ExpireAt.Time); err != nil {
27 | return nil, err
28 | }
29 | }
30 | if ent.LastUsedAt.Valid {
31 | if lastUsedAt, err = ptypes.TimestampProto(ent.LastUsedAt.Time); err != nil {
32 | return nil, err
33 | }
34 | }
35 |
36 | return &charonrpc.RefreshToken{
37 | Token: ent.Token,
38 | Notes: &ent.Notes,
39 | Revoked: ent.Revoked,
40 | ExpireAt: expireAt,
41 | LastUsedAt: lastUsedAt,
42 | UserId: ent.UserID,
43 | CreatedAt: createdAt,
44 | CreatedBy: &ent.CreatedBy,
45 | UpdatedAt: updatedAt,
46 | UpdatedBy: &ent.UpdatedBy,
47 | }, nil
48 | }
49 |
50 | func ReverseRefreshTokens(in []*model.RefreshTokenEntity) ([]*charonrpc.RefreshToken, error) {
51 | res := make([]*charonrpc.RefreshToken, 0, len(in))
52 | for _, ent := range in {
53 | msg, err := ReverseRefreshToken(ent)
54 | if err != nil {
55 | return nil, err
56 | }
57 | res = append(res, msg)
58 | }
59 |
60 | return res, nil
61 | }
62 |
63 | func RefreshTokenQuery(q *charonrpc.RefreshTokenQuery) *model.RefreshTokenCriteria {
64 | var revoked ntypes.Bool
65 | if q.GetRevoked() != nil {
66 | revoked = *q.GetRevoked()
67 | }
68 |
69 | return &model.RefreshTokenCriteria{
70 | UserID: q.GetUserId(),
71 | Notes: q.GetNotes(),
72 | Revoked: revoked, // TODO: pointer?
73 | ExpireAt: q.GetExpireAt(),
74 | LastUsedAt: q.GetLastUsedAt(),
75 | CreatedAt: q.GetCreatedAt(),
76 | UpdatedAt: q.GetUpdatedAt(),
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/internal/charond/handler_modify_group.go:
--------------------------------------------------------------------------------
1 | package charond
2 |
3 | import (
4 | "context"
5 | "database/sql"
6 |
7 | "github.com/piotrkowalczuk/charon"
8 | charonrpc "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1"
9 | "github.com/piotrkowalczuk/charon/internal/grpcerr"
10 | "github.com/piotrkowalczuk/charon/internal/mapping"
11 | "github.com/piotrkowalczuk/charon/internal/model"
12 | "github.com/piotrkowalczuk/charon/internal/session"
13 | "github.com/piotrkowalczuk/ntypes"
14 | "google.golang.org/grpc/codes"
15 | )
16 |
17 | type modifyGroupHandler struct {
18 | *handler
19 | }
20 |
21 | func (mgh *modifyGroupHandler) Modify(ctx context.Context, req *charonrpc.ModifyGroupRequest) (*charonrpc.ModifyGroupResponse, error) {
22 | if req.Id <= 0 {
23 | return nil, grpcerr.E(codes.InvalidArgument, "group id is missing")
24 | }
25 | if !req.GetName().GetValid() && !req.GetDescription().GetValid() {
26 | return nil, grpcerr.E(codes.InvalidArgument, "nothing to be modified")
27 | }
28 | act, err := mgh.Actor(ctx)
29 | if err != nil {
30 | return nil, err
31 | }
32 |
33 | if err = mgh.firewall(req, act); err != nil {
34 | return nil, err
35 | }
36 |
37 | group, err := mgh.repository.group.UpdateOneByID(ctx, req.Id, &model.GroupPatch{
38 | UpdatedBy: ntypes.Int64{Int64: act.User.ID, Valid: true},
39 | Name: allocNilString(req.Name),
40 | Description: allocNilString(req.Description),
41 | })
42 | if err != nil {
43 | if err == sql.ErrNoRows {
44 | return nil, grpcerr.E(codes.NotFound, "group does not exists")
45 | }
46 | return nil, grpcerr.E(codes.Internal, "update group by id query failed", err)
47 | }
48 |
49 | return mgh.response(group)
50 | }
51 |
52 | func (mgh *modifyGroupHandler) firewall(req *charonrpc.ModifyGroupRequest, act *session.Actor) error {
53 | if act.User.IsSuperuser {
54 | return nil
55 | }
56 | if act.Permissions.Contains(charon.GroupCanModify) {
57 | return nil
58 | }
59 |
60 | return grpcerr.E(codes.PermissionDenied, "group cannot be modified, missing permission")
61 | }
62 |
63 | func (mgh *modifyGroupHandler) response(ent *model.GroupEntity) (*charonrpc.ModifyGroupResponse, error) {
64 | msg, err := mapping.ReverseGroup(ent)
65 | if err != nil {
66 | return nil, grpcerr.E(codes.Internal, "group reverse mapping failure")
67 | }
68 | return &charonrpc.ModifyGroupResponse{Group: msg}, nil
69 | }
70 |
--------------------------------------------------------------------------------
/pkg/securitycontext/security_context.go:
--------------------------------------------------------------------------------
1 | package securitycontext
2 |
3 | import (
4 | "context"
5 | "errors"
6 |
7 | "github.com/piotrkowalczuk/charon"
8 | "github.com/piotrkowalczuk/mnemosyne"
9 | "golang.org/x/oauth2"
10 | )
11 |
12 | // Context ....
13 | type Context interface {
14 | context.Context
15 | oauth2.TokenSource
16 | // Actor ...
17 | Actor() (Actor, bool)
18 | // AccessToken ...
19 | AccessToken() (string, bool)
20 | }
21 |
22 | type securityContext struct {
23 | context.Context
24 | }
25 |
26 | // NewSecurityContext allocates new context.
27 | func NewSecurityContext(ctx context.Context) Context {
28 | return &securityContext{Context: ctx}
29 | }
30 |
31 | // Actor implements Context interface.
32 | func (sc *securityContext) Actor() (Actor, bool) {
33 | return ActorFromContext(sc)
34 | }
35 |
36 | // AccessToken implements Context interface.
37 | func (sc *securityContext) AccessToken() (string, bool) {
38 | return mnemosyne.AccessTokenFromContext(sc.Context)
39 | }
40 |
41 | // Token implements oauth2.TokenSource interface.
42 | func (sc *securityContext) Token() (*oauth2.Token, error) {
43 | at, ok := sc.AccessToken()
44 | if !ok {
45 | return nil, errors.New("securitycontext: missing access token, oauth2 token cannot be returned")
46 | }
47 | return &oauth2.Token{
48 | AccessToken: at,
49 | }, nil
50 | }
51 |
52 | // Actor is a generic object that represent anything that can be under control of charon.
53 | type Actor struct {
54 | ID int64 `json:"id"`
55 | Username string `json:"username"`
56 | FirstName string `json:"firstName"`
57 | LastName string `json:"lastName"`
58 | IsSuperuser bool `json:"isSuperuser"`
59 | IsActive bool `json:"isActive"`
60 | IsStaff bool `json:"isStaff"`
61 | IsConfirmed bool `json:"isConfirmed"`
62 | Permissions charon.Permissions `json:"permissions"`
63 | }
64 |
65 | type key struct{}
66 |
67 | var contextKeyActor = key{}
68 |
69 | // NewActorContext returns a new Context that carries Actor value.
70 | func NewActorContext(ctx context.Context, act Actor) context.Context {
71 | return context.WithValue(ctx, contextKeyActor, act)
72 | }
73 |
74 | // ActorFromContext returns the Actor value stored in context, if any.
75 | func ActorFromContext(ctx context.Context) (Actor, bool) {
76 | act, ok := ctx.Value(contextKeyActor).(Actor)
77 | return act, ok
78 | }
79 |
--------------------------------------------------------------------------------
/internal/charond/daemon_test.go:
--------------------------------------------------------------------------------
1 | package charond
2 |
3 | import (
4 | "net"
5 | "testing"
6 |
7 | "go.uber.org/zap"
8 |
9 | "github.com/piotrkowalczuk/mnemosyne/mnemosyned"
10 | "golang.org/x/crypto/bcrypt"
11 | )
12 |
13 | func TestDaemon_Run(t *testing.T) {
14 | certPath := "../../data/test-selfsigned.crt"
15 | keyPath := "../../data/test-selfsigned.key"
16 |
17 | mnemosyneRPCListener, err := net.Listen("tcp", "localhost:0") // any available address
18 | if err != nil {
19 | t.Fatalf("mnemosyne daemon tcp listener setup error: %s", err.Error())
20 | }
21 | mnemosyneDaemon, err := mnemosyned.NewDaemon(&mnemosyned.DaemonOpts{
22 | IsTest: true,
23 | ClusterListenAddr: mnemosyneRPCListener.Addr().String(),
24 | Logger: zap.L(),
25 | PostgresAddress: testPostgresAddress,
26 | PostgresTable: "session",
27 | PostgresSchema: "mnemosyne",
28 | RPCListener: mnemosyneRPCListener,
29 | TLS: true,
30 | TLSKeyFile: keyPath,
31 | TLSCertFile: certPath,
32 | })
33 | if err != nil {
34 | t.Fatalf("mnemosyne daemon cannot be instantiated: %s", err.Error())
35 | }
36 | if err := mnemosyneDaemon.Run(); err != nil {
37 | t.Fatalf("mnemosyne daemon start error: %s", err.Error())
38 | }
39 |
40 | charonRPCListener, err := net.Listen("tcp", "localhost:0") // any available address
41 | if err != nil {
42 | t.Fatalf("charon daemon tcp listener setup error: %s", err.Error())
43 | }
44 | debugRPCListener, err := net.Listen("tcp", "localhost:0") // any available address
45 | if err != nil {
46 | t.Fatalf("charon daemon tcp listener setup error: %s", err.Error())
47 | }
48 |
49 | logger := zap.L()
50 |
51 | d := NewDaemon(DaemonOpts{
52 | Test: true,
53 | Monitoring: false,
54 | MnemosyneAddress: mnemosyneDaemon.Addr().String(),
55 | MnemosyneTLS: true,
56 | MnemosyneTLSCertFile: certPath,
57 | Logger: logger,
58 | PostgresAddress: testPostgresAddress,
59 | RPCListener: charonRPCListener,
60 | DebugListener: debugRPCListener,
61 | PasswordBCryptCost: bcrypt.MinCost,
62 | TLS: true,
63 | TLSKeyFile: keyPath,
64 | TLSCertFile: certPath,
65 | })
66 | if err := d.Run(); err != nil {
67 | t.Fatalf("charon daemon start error: %s", err.Error())
68 | }
69 | if err := d.Close(); err != nil {
70 | t.Fatalf("charon daemon close error: %s", err.Error())
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/example/client/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "flag"
6 | "fmt"
7 | "log"
8 | "time"
9 |
10 | "github.com/golang/protobuf/ptypes/wrappers"
11 | "github.com/piotrkowalczuk/charon"
12 | charonrpc "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1"
13 | "google.golang.org/grpc"
14 | "google.golang.org/grpc/metadata"
15 | "google.golang.org/grpc/status"
16 | )
17 |
18 | const (
19 | permissionCommentCanCreate charon.Permission = "forumservice:comment:can create"
20 | permissionCommentCanEditAsOwner charon.Permission = "forumservice:comment:can edit as an owner"
21 | permissionCommentCanEditAsStranger charon.Permission = "forumservice:comment:can edit as a stranger"
22 | )
23 |
24 | var (
25 | address string
26 | token string
27 | )
28 |
29 | func init() {
30 | flag.StringVar(&address, "address", "localhost:8010", "charond service address")
31 | flag.StringVar(&token, "token", "", "session token")
32 | }
33 |
34 | func main() {
35 | flag.Parse()
36 |
37 | if token == "" {
38 | log.Fatal("missing session token")
39 | }
40 |
41 | conn, err := grpc.Dial(address, grpc.WithBlock(), grpc.WithInsecure(), grpc.WithTimeout(2*time.Second))
42 | if err != nil {
43 | log.Fatal(status.Convert(err).Message())
44 | }
45 | defer conn.Close()
46 |
47 | rpc := charonrpc.NewPermissionManagerClient(conn)
48 | if _, err = rpc.Register(context.Background(), &charonrpc.RegisterPermissionsRequest{
49 | Permissions: []string{
50 | permissionCommentCanCreate.String(),
51 | permissionCommentCanEditAsOwner.String(),
52 | permissionCommentCanEditAsStranger.String(),
53 | },
54 | }); err != nil {
55 | log.Fatal(err)
56 | }
57 |
58 | ctx := metadata.NewOutgoingContext(context.Background(), metadata.Pairs("request_id", "123456789"))
59 | res, err := charonrpc.NewAuthClient(conn).Actor(ctx, &wrappers.StringValue{Value: token})
60 | if err != nil {
61 | log.Fatalf("%s: %s", status.Code(err).String(), status.Convert(err).Message())
62 | }
63 |
64 | fmt.Printf("id: %d \n", res.Id)
65 | fmt.Printf("username: %s \n", res.Username)
66 | fmt.Printf("first name: %s \n", res.FirstName)
67 | fmt.Printf("last name: %s \n", res.LastName)
68 | fmt.Printf("is active: %t \n", res.IsActive)
69 | fmt.Printf("is confirmed: %t \n", res.IsConfirmed)
70 | fmt.Printf("is staff: %t \n", res.IsStuff)
71 | fmt.Printf("is superuser: %t \n", res.IsSuperuser)
72 | if len(res.Permissions) > 0 {
73 | fmt.Println("permissions:")
74 | }
75 | for _, p := range res.Permissions {
76 | fmt.Printf(" - %s \n", p)
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/Gopkg.toml:
--------------------------------------------------------------------------------
1 |
2 | # Gopkg.toml example
3 | #
4 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
5 | # for detailed Gopkg.toml documentation.
6 | #
7 | # required = ["github.com/user/thing/cmd/thing"]
8 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
9 | #
10 | # [[constraint]]
11 | # name = "github.com/user/project"
12 | # version = "1.0.0"
13 | #
14 | # [[constraint]]
15 | # name = "github.com/user/project2"
16 | # branch = "dev"
17 | # source = "github.com/myfork/project2"
18 | #
19 | # [[override]]
20 | # name = "github.com/x/y"
21 | # version = "2.4.0"
22 |
23 | ignored = ["github.com/piotrkowalczuk/charon/data"]
24 |
25 | [[constraint]]
26 | name = "github.com/golang/protobuf"
27 | version = "^1.2.0"
28 |
29 | [[constraint]]
30 | name = "github.com/lib/pq"
31 | version = "^1.0.0"
32 |
33 | [[constraint]]
34 | name = "github.com/google/uuid"
35 | version = "^1.0.0"
36 |
37 | [[constraint]]
38 | name = "github.com/piotrkowalczuk/zapstackdriver"
39 | version = "^1.0.0"
40 |
41 | [[constraint]]
42 | name = "github.com/piotrkowalczuk/mnemosyne"
43 | version = "~0.18.0"
44 |
45 | [[constraint]]
46 | name = "github.com/piotrkowalczuk/ntypes"
47 | version = "^1.0.0"
48 |
49 | [[constraint]]
50 | name = "github.com/piotrkowalczuk/promgrpc"
51 | version = "^3.2.0"
52 |
53 | [[constraint]]
54 | name = "github.com/piotrkowalczuk/qtypes"
55 | version = "~0.3.6"
56 |
57 | [[constraint]]
58 | name = "github.com/prometheus/client_golang"
59 | version = "~0.8.0"
60 |
61 | [[constraint]]
62 | name = "github.com/smartystreets/goconvey"
63 | version = "^1.6.2"
64 |
65 | [[constraint]]
66 | name = "github.com/stretchr/testify"
67 | version = "^1.1.4"
68 |
69 | [[constraint]]
70 | name = "go.uber.org/zap"
71 | version = "^1.0.0"
72 |
73 | [[constraint]]
74 | name = "golang.org/x/crypto"
75 | branch = "master"
76 |
77 | [[constraint]]
78 | name = "golang.org/x/net"
79 | branch = "master"
80 |
81 | [[constraint]]
82 | name = "golang.org/x/oauth2"
83 | branch = "master"
84 |
85 | [[constraint]]
86 | name = "google.golang.org/grpc"
87 | version = "^1.16.0"
88 |
89 | [[constraint]]
90 | name = "github.com/piotrkowalczuk/pqt"
91 | version = "~0.27.0"
92 |
93 | [[constraint]]
94 | name = "github.com/piotrkowalczuk/qtypespqt"
95 | version = "~0.4.0"
96 |
97 | [[constraint]]
98 | name = "github.com/piotrkowalczuk/ntypespqt"
99 | version = "~0.2.6"
100 |
--------------------------------------------------------------------------------
/pkg/securitycontext/security_context_test.go:
--------------------------------------------------------------------------------
1 | package securitycontext
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "reflect"
7 | "testing"
8 |
9 | "github.com/piotrkowalczuk/mnemosyne"
10 | "golang.org/x/oauth2"
11 | )
12 |
13 | func ExampleSecurityContext() {
14 | token := "0000000001some hash"
15 | subject := Actor{
16 | ID: 1,
17 | Username: "j.kowalski@gmail.com",
18 | }
19 | ctx := NewActorContext(context.Background(), subject)
20 | ctx = mnemosyne.NewAccessTokenContext(ctx, token)
21 | sctx := NewSecurityContext(ctx)
22 |
23 | var (
24 | t *oauth2.Token
25 | act Actor
26 | err error
27 | ok bool
28 | )
29 | if t, err = sctx.Token(); err != nil {
30 | fmt.Printf("unexpected error: %s", err.Error())
31 | } else {
32 | fmt.Println(t.AccessToken)
33 | }
34 | if act, ok = sctx.Actor(); ok {
35 | fmt.Println(act.ID)
36 | fmt.Println(act.Username)
37 | }
38 |
39 | // Output:
40 | // 0000000001some hash
41 | // 1
42 | // j.kowalski@gmail.com
43 | }
44 |
45 | func TestNewSecurityContext(t *testing.T) {
46 | sctx := NewSecurityContext(context.Background())
47 |
48 | if _, ok := sctx.(Context); !ok {
49 | t.Error("result should imeplement Context interface")
50 | }
51 | }
52 |
53 | func TestSecurityContext_Actor(t *testing.T) {
54 | expectedActor := Actor{ID: 1}
55 | ctx := NewActorContext(context.Background(), expectedActor)
56 | sctx := NewSecurityContext(ctx)
57 |
58 | act, ok := sctx.Actor()
59 | if ok {
60 | if !reflect.DeepEqual(act, expectedActor) {
61 | t.Error("provided and retrieved subject should be the same")
62 | }
63 | } else {
64 | t.Error("actor should be able retrieved")
65 | }
66 | }
67 |
68 | func TestSecurityContext_Actor_empty(t *testing.T) {
69 | sctx := NewSecurityContext(context.Background())
70 |
71 | _, ok := sctx.Actor()
72 | if ok {
73 | t.Error("subject should not be there")
74 | }
75 | }
76 |
77 | func TestSecurityContext_Token(t *testing.T) {
78 | expectedToken := "00000000011"
79 | ctx := mnemosyne.NewAccessTokenContext(context.Background(), expectedToken)
80 | sctx := NewSecurityContext(ctx)
81 |
82 | token, err := sctx.Token()
83 | if err != nil {
84 | t.Fatalf("unexpected error: %s", err.Error())
85 | }
86 | if token.AccessToken != string(expectedToken) {
87 | t.Error("provided and retrieved token should be the same")
88 | }
89 | }
90 |
91 | func TestSecurityContext_Token_empty(t *testing.T) {
92 | sctx := NewSecurityContext(context.Background())
93 |
94 | _, err := sctx.Token()
95 | if err == nil {
96 | t.Error("expected error, got nil")
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/internal/password/password_test.go:
--------------------------------------------------------------------------------
1 | package password_test
2 |
3 | import (
4 | "bytes"
5 | "testing"
6 |
7 | "github.com/piotrkowalczuk/charon/internal/password"
8 |
9 | "golang.org/x/crypto/bcrypt"
10 | )
11 |
12 | func TestNewBCryptHasher_bellowMin(t *testing.T) {
13 | ph, err := password.NewBCryptHasher(bcrypt.MinCost - 1)
14 |
15 | testNewBCryptHasherFailure(t, ph, err)
16 | }
17 |
18 | func TestNewBCryptHasher_min(t *testing.T) {
19 | ph, err := password.NewBCryptHasher(bcrypt.MinCost)
20 |
21 | testNewBCryptHasherSuccess(t, ph, err)
22 | }
23 |
24 | func TestNewBCryptHasher_between(t *testing.T) {
25 | ph, err := password.NewBCryptHasher(25)
26 |
27 | testNewBCryptHasherSuccess(t, ph, err)
28 | }
29 |
30 | func TestNewBCryptHasher_max(t *testing.T) {
31 | ph, err := password.NewBCryptHasher(bcrypt.MaxCost)
32 |
33 | testNewBCryptHasherSuccess(t, ph, err)
34 | }
35 |
36 | func TestNewBCryptHasher_aboveMax(t *testing.T) {
37 | ph, err := password.NewBCryptHasher(bcrypt.MaxCost + 1)
38 |
39 | testNewBCryptHasherFailure(t, ph, err)
40 | }
41 |
42 | func testNewBCryptHasherSuccess(t *testing.T, ph password.Hasher, err error) {
43 | if err != nil {
44 | t.Fatalf("unexpected error %s", err.Error())
45 | }
46 | if ph == nil {
47 | t.Error("password hasher should not be nil")
48 | }
49 | }
50 |
51 | func testNewBCryptHasherFailure(t *testing.T, ph password.Hasher, err error) {
52 | if err == nil {
53 | t.Error("error expected")
54 | }
55 | if ph != nil {
56 | t.Errorf("password hasher should be nil, but got %v", ph)
57 | }
58 | }
59 |
60 | func TestBCryptHasher_Hash(t *testing.T) {
61 | ph, err := password.NewBCryptHasher(10)
62 | if err != nil {
63 | t.Fatalf("unexpected error: %s", err.Error())
64 | }
65 |
66 | given := []byte("123")
67 | expected := []byte("$2a$10$NF5jon4vHytVzwVz5wKAe.AycwRQ8mmeRXEoxTMu4kh4He7K1YCRe")
68 |
69 | got, err := ph.Hash(given)
70 | if err != nil {
71 | t.Fatalf("hash returned unexpected error: %s", err.Error())
72 | }
73 | if len(expected) != len(got) {
74 | t.Errorf("length of hash do not match, expected %d but got %d", len(expected), len(got))
75 | }
76 | if !bytes.HasPrefix(got, expected[:7]) {
77 | t.Errorf("hash should have prefix %s but does not: %s", expected[:7], got)
78 | }
79 | }
80 |
81 | func TestBCryptHasher_Compare(t *testing.T) {
82 | given := []byte("123")
83 |
84 | ph, err := password.NewBCryptHasher(10)
85 | if err != nil {
86 | t.Fatalf("unexpected error: %s", err.Error())
87 | }
88 |
89 | got, err := ph.Hash(given)
90 | if err != nil {
91 | t.Fatalf("hash returned unexpected error: %s", err.Error())
92 | }
93 |
94 | if !ph.Compare(got, given) {
95 | t.Error("password do not match")
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/pb/rpc/charond/v1mock/permission_manager_server.go:
--------------------------------------------------------------------------------
1 | // Code generated by mockery v1.0.0. DO NOT EDIT.
2 |
3 | package charondmock
4 |
5 | import charond "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1"
6 | import context "context"
7 | import mock "github.com/stretchr/testify/mock"
8 |
9 | // PermissionManagerServer is an autogenerated mock type for the PermissionManagerServer type
10 | type PermissionManagerServer struct {
11 | mock.Mock
12 | }
13 |
14 | // Get provides a mock function with given fields: _a0, _a1
15 | func (_m *PermissionManagerServer) Get(_a0 context.Context, _a1 *charond.GetPermissionRequest) (*charond.GetPermissionResponse, error) {
16 | ret := _m.Called(_a0, _a1)
17 |
18 | var r0 *charond.GetPermissionResponse
19 | if rf, ok := ret.Get(0).(func(context.Context, *charond.GetPermissionRequest) *charond.GetPermissionResponse); ok {
20 | r0 = rf(_a0, _a1)
21 | } else {
22 | if ret.Get(0) != nil {
23 | r0 = ret.Get(0).(*charond.GetPermissionResponse)
24 | }
25 | }
26 |
27 | var r1 error
28 | if rf, ok := ret.Get(1).(func(context.Context, *charond.GetPermissionRequest) error); ok {
29 | r1 = rf(_a0, _a1)
30 | } else {
31 | r1 = ret.Error(1)
32 | }
33 |
34 | return r0, r1
35 | }
36 |
37 | // List provides a mock function with given fields: _a0, _a1
38 | func (_m *PermissionManagerServer) List(_a0 context.Context, _a1 *charond.ListPermissionsRequest) (*charond.ListPermissionsResponse, error) {
39 | ret := _m.Called(_a0, _a1)
40 |
41 | var r0 *charond.ListPermissionsResponse
42 | if rf, ok := ret.Get(0).(func(context.Context, *charond.ListPermissionsRequest) *charond.ListPermissionsResponse); ok {
43 | r0 = rf(_a0, _a1)
44 | } else {
45 | if ret.Get(0) != nil {
46 | r0 = ret.Get(0).(*charond.ListPermissionsResponse)
47 | }
48 | }
49 |
50 | var r1 error
51 | if rf, ok := ret.Get(1).(func(context.Context, *charond.ListPermissionsRequest) error); ok {
52 | r1 = rf(_a0, _a1)
53 | } else {
54 | r1 = ret.Error(1)
55 | }
56 |
57 | return r0, r1
58 | }
59 |
60 | // Register provides a mock function with given fields: _a0, _a1
61 | func (_m *PermissionManagerServer) Register(_a0 context.Context, _a1 *charond.RegisterPermissionsRequest) (*charond.RegisterPermissionsResponse, error) {
62 | ret := _m.Called(_a0, _a1)
63 |
64 | var r0 *charond.RegisterPermissionsResponse
65 | if rf, ok := ret.Get(0).(func(context.Context, *charond.RegisterPermissionsRequest) *charond.RegisterPermissionsResponse); ok {
66 | r0 = rf(_a0, _a1)
67 | } else {
68 | if ret.Get(0) != nil {
69 | r0 = ret.Get(0).(*charond.RegisterPermissionsResponse)
70 | }
71 | }
72 |
73 | var r1 error
74 | if rf, ok := ret.Get(1).(func(context.Context, *charond.RegisterPermissionsRequest) error); ok {
75 | r1 = rf(_a0, _a1)
76 | } else {
77 | r1 = ret.Error(1)
78 | }
79 |
80 | return r0, r1
81 | }
82 |
--------------------------------------------------------------------------------
/cmd/charond/config.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "os"
7 | )
8 |
9 | var version string
10 |
11 | type configuration struct {
12 | host string
13 | port int
14 | test bool
15 | logger struct {
16 | environment string
17 | level string
18 | }
19 | mnemosyned struct {
20 | address string
21 | tls struct {
22 | enabled bool
23 | certFile string
24 | }
25 | }
26 | password struct {
27 | strategy string
28 | bcrypt struct {
29 | cost int
30 | }
31 | }
32 | monitoring struct {
33 | enabled bool
34 | }
35 | postgres struct {
36 | address string
37 | debug bool
38 | }
39 | tls struct {
40 | enabled bool
41 | certFile string
42 | keyFile string
43 | }
44 | }
45 |
46 | func (c *configuration) init() {
47 | if c == nil {
48 | *c = configuration{}
49 | }
50 |
51 | flag.StringVar(&c.host, "host", "127.0.0.1", "host")
52 | flag.IntVar(&c.port, "port", 8080, "port")
53 | flag.BoolVar(&c.test, "test", false, "determines in what mode application starts")
54 | // LOGGER
55 | flag.StringVar(&c.logger.environment, "log.environment", "production", "Logger environment config (production, stackdriver or development).")
56 | flag.StringVar(&c.logger.level, "log.level", "info", "Logger level (debug, info, warn, error, dpanic, panic, fatal)")
57 | // MNEMOSYNE
58 | flag.StringVar(&c.mnemosyned.address, "mnemosyned.address", "mnemosyned:8080", "mnemosyne daemon session store connection address")
59 | flag.BoolVar(&c.mnemosyned.tls.enabled, "mnemosyned.tls", false, "tls enable flag for mnemosyned client connection")
60 | flag.StringVar(&c.mnemosyned.tls.certFile, "mnemosyned.tls.crt", "", "path to tls cert file for mnemosyned client connection")
61 | // PASSWORD
62 | flag.StringVar(&c.password.strategy, "password.strategy", "bcrypt", "strategy how password will be stored")
63 | flag.IntVar(&c.password.bcrypt.cost, "password.bcryptcost", 10, "bcrypt cost, bigget than safer (and longer to create)")
64 | flag.BoolVar(&c.monitoring.enabled, "monitoring", false, "toggle application monitoring")
65 | // POSTGRES
66 | flag.StringVar(&c.postgres.address, "postgres.address", "postgres://postgres:postgres@postgres/postgres?sslmode=disable", "postgres connection string")
67 | flag.BoolVar(&c.postgres.debug, "postgres.debug", false, "if true database queries are logged")
68 | // TLS
69 | flag.BoolVar(&c.tls.enabled, "tls", false, "tls enable flag")
70 | flag.StringVar(&c.tls.certFile, "tls.crt", "", "path to tls cert file")
71 | flag.StringVar(&c.tls.keyFile, "tls.key", "", "path to tls key file")
72 | }
73 |
74 | func (c *configuration) parse() {
75 | if !flag.Parsed() {
76 | ver := flag.Bool("version", false, "print version and exit")
77 | flag.Parse()
78 | if *ver {
79 | fmt.Printf("%s\n", version)
80 | os.Exit(0)
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/pb/rpc/charond/v1/group.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package charon.rpc.charond.v1;
4 |
5 | option go_package = "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1;charond";
6 | option java_multiple_files = true;
7 | option java_package = "com.github.charon.rpc.charond.v1";
8 |
9 | import "google/protobuf/timestamp.proto";
10 | import "google/protobuf/wrappers.proto";
11 | import "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1/common.proto";
12 | import "ntypes/ntypes.proto";
13 |
14 | service GroupManager {
15 | rpc Create(CreateGroupRequest) returns (CreateGroupResponse) {};
16 | rpc Modify(ModifyGroupRequest) returns (ModifyGroupResponse) {};
17 | rpc Get(GetGroupRequest) returns (GetGroupResponse) {};
18 | rpc List(ListGroupsRequest) returns (ListGroupsResponse) {};
19 | rpc Delete(DeleteGroupRequest) returns (google.protobuf.BoolValue) {};
20 |
21 | rpc ListPermissions(ListGroupPermissionsRequest) returns (ListGroupPermissionsResponse) {};
22 | rpc SetPermissions(SetGroupPermissionsRequest) returns (SetGroupPermissionsResponse) {};
23 | }
24 |
25 | message Group {
26 | int64 id = 1;
27 | string name = 2;
28 | string description = 3;
29 | google.protobuf.Timestamp created_at = 4;
30 | ntypes.Int64 created_by = 5;
31 | google.protobuf.Timestamp updated_at = 6;
32 | ntypes.Int64 updated_by = 7;
33 | }
34 |
35 | message CreateGroupRequest {
36 | string name = 1;
37 | ntypes.String description = 2;
38 | }
39 |
40 | message CreateGroupResponse {
41 | Group group = 1;
42 | }
43 |
44 | message GetGroupRequest {
45 | int64 id = 1;
46 | }
47 |
48 | message GetGroupResponse {
49 | Group group = 1;
50 | }
51 |
52 | message ListGroupsRequest {
53 | reserved 1 to 99;
54 | ntypes.Int64 offset = 100;
55 | ntypes.Int64 limit = 101;
56 | repeated Order order_by = 102;
57 | }
58 |
59 | message ListGroupsResponse {
60 | repeated Group groups = 1;
61 | }
62 |
63 | message DeleteGroupRequest {
64 | int64 id = 1;
65 | }
66 |
67 | message ModifyGroupRequest {
68 | int64 id = 1;
69 | ntypes.String name = 2;
70 | ntypes.String description = 3;
71 | }
72 |
73 | message ModifyGroupResponse {
74 | Group group = 1;
75 | }
76 |
77 | message SetGroupPermissionsRequest {
78 | int64 group_id = 1;
79 | repeated string permissions = 2;
80 | // Force tells if permission should be created in case if it does not exists.
81 | bool force = 3;
82 | }
83 |
84 | message SetGroupPermissionsResponse {
85 | int64 created = 1;
86 | int64 removed = 2;
87 | int64 untouched = 3;
88 | }
89 |
90 | message ListGroupPermissionsRequest {
91 | int64 id = 1;
92 | }
93 |
94 | message ListGroupPermissionsResponse {
95 | repeated string permissions = 1;
96 | }
--------------------------------------------------------------------------------
/pb/rpc/charond/v1mock/refresh_token_manager_server.go:
--------------------------------------------------------------------------------
1 | // Code generated by mockery v1.0.0. DO NOT EDIT.
2 |
3 | package charondmock
4 |
5 | import charond "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1"
6 | import context "context"
7 | import mock "github.com/stretchr/testify/mock"
8 |
9 | // RefreshTokenManagerServer is an autogenerated mock type for the RefreshTokenManagerServer type
10 | type RefreshTokenManagerServer struct {
11 | mock.Mock
12 | }
13 |
14 | // Create provides a mock function with given fields: _a0, _a1
15 | func (_m *RefreshTokenManagerServer) Create(_a0 context.Context, _a1 *charond.CreateRefreshTokenRequest) (*charond.CreateRefreshTokenResponse, error) {
16 | ret := _m.Called(_a0, _a1)
17 |
18 | var r0 *charond.CreateRefreshTokenResponse
19 | if rf, ok := ret.Get(0).(func(context.Context, *charond.CreateRefreshTokenRequest) *charond.CreateRefreshTokenResponse); ok {
20 | r0 = rf(_a0, _a1)
21 | } else {
22 | if ret.Get(0) != nil {
23 | r0 = ret.Get(0).(*charond.CreateRefreshTokenResponse)
24 | }
25 | }
26 |
27 | var r1 error
28 | if rf, ok := ret.Get(1).(func(context.Context, *charond.CreateRefreshTokenRequest) error); ok {
29 | r1 = rf(_a0, _a1)
30 | } else {
31 | r1 = ret.Error(1)
32 | }
33 |
34 | return r0, r1
35 | }
36 |
37 | // List provides a mock function with given fields: _a0, _a1
38 | func (_m *RefreshTokenManagerServer) List(_a0 context.Context, _a1 *charond.ListRefreshTokensRequest) (*charond.ListRefreshTokensResponse, error) {
39 | ret := _m.Called(_a0, _a1)
40 |
41 | var r0 *charond.ListRefreshTokensResponse
42 | if rf, ok := ret.Get(0).(func(context.Context, *charond.ListRefreshTokensRequest) *charond.ListRefreshTokensResponse); ok {
43 | r0 = rf(_a0, _a1)
44 | } else {
45 | if ret.Get(0) != nil {
46 | r0 = ret.Get(0).(*charond.ListRefreshTokensResponse)
47 | }
48 | }
49 |
50 | var r1 error
51 | if rf, ok := ret.Get(1).(func(context.Context, *charond.ListRefreshTokensRequest) error); ok {
52 | r1 = rf(_a0, _a1)
53 | } else {
54 | r1 = ret.Error(1)
55 | }
56 |
57 | return r0, r1
58 | }
59 |
60 | // Revoke provides a mock function with given fields: _a0, _a1
61 | func (_m *RefreshTokenManagerServer) Revoke(_a0 context.Context, _a1 *charond.RevokeRefreshTokenRequest) (*charond.RevokeRefreshTokenResponse, error) {
62 | ret := _m.Called(_a0, _a1)
63 |
64 | var r0 *charond.RevokeRefreshTokenResponse
65 | if rf, ok := ret.Get(0).(func(context.Context, *charond.RevokeRefreshTokenRequest) *charond.RevokeRefreshTokenResponse); ok {
66 | r0 = rf(_a0, _a1)
67 | } else {
68 | if ret.Get(0) != nil {
69 | r0 = ret.Get(0).(*charond.RevokeRefreshTokenResponse)
70 | }
71 | }
72 |
73 | var r1 error
74 | if rf, ok := ret.Get(1).(func(context.Context, *charond.RevokeRefreshTokenRequest) error); ok {
75 | r1 = rf(_a0, _a1)
76 | } else {
77 | r1 = ret.Error(1)
78 | }
79 |
80 | return r0, r1
81 | }
82 |
--------------------------------------------------------------------------------
/internal/charond/handler_get_user.go:
--------------------------------------------------------------------------------
1 | package charond
2 |
3 | import (
4 | "context"
5 | "database/sql"
6 |
7 | "github.com/piotrkowalczuk/charon"
8 | charonrpc "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1"
9 | "github.com/piotrkowalczuk/charon/internal/grpcerr"
10 | "github.com/piotrkowalczuk/charon/internal/mapping"
11 | "github.com/piotrkowalczuk/charon/internal/model"
12 | "github.com/piotrkowalczuk/charon/internal/session"
13 |
14 | "google.golang.org/grpc/codes"
15 | )
16 |
17 | type getUserHandler struct {
18 | *handler
19 | }
20 |
21 | func (guh *getUserHandler) Get(ctx context.Context, req *charonrpc.GetUserRequest) (*charonrpc.GetUserResponse, error) {
22 | if req.Id <= 0 {
23 | return nil, grpcerr.E(codes.InvalidArgument, "user id is missing")
24 | }
25 |
26 | act, err := guh.Actor(ctx)
27 | if err != nil {
28 | return nil, err
29 | }
30 | ent, err := guh.repository.user.FindOneByID(ctx, req.Id)
31 | if err != nil {
32 | if err == sql.ErrNoRows {
33 | return nil, grpcerr.E(codes.NotFound, "user does not exists")
34 | }
35 | return nil, grpcerr.E(codes.Internal, "user cannot be fetched", err)
36 | }
37 | if err = guh.firewall(req, act, ent); err != nil {
38 | return nil, err
39 | }
40 |
41 | return guh.response(ent)
42 | }
43 |
44 | func (guh *getUserHandler) firewall(req *charonrpc.GetUserRequest, act *session.Actor, ent *model.UserEntity) error {
45 | if act.User.IsSuperuser {
46 | return nil
47 | }
48 | if ent.IsSuperuser {
49 | return grpcerr.E(codes.PermissionDenied, "only superuser is permitted to retrieve other superuser")
50 | }
51 | if ent.IsStaff {
52 | if ent.CreatedBy.Int64Or(0) == act.User.ID {
53 | if !act.Permissions.Contains(charon.UserCanRetrieveStaffAsOwner) {
54 | return grpcerr.E(codes.PermissionDenied, "staff user cannot be retrieved as an owner, missing permission")
55 | }
56 | return nil
57 | }
58 | if !act.Permissions.Contains(charon.UserCanRetrieveStaffAsStranger) {
59 | return grpcerr.E(codes.PermissionDenied, "staff user cannot be retrieved as a stranger, missing permission")
60 | }
61 | return nil
62 | }
63 | if ent.CreatedBy.Int64Or(0) == act.User.ID {
64 | if !act.Permissions.Contains(charon.UserCanRetrieveAsOwner) {
65 | return grpcerr.E(codes.PermissionDenied, "user cannot be retrieved as an owner, missing permission")
66 | }
67 | return nil
68 | }
69 | if !act.Permissions.Contains(charon.UserCanRetrieveAsStranger) {
70 | return grpcerr.E(codes.PermissionDenied, "user cannot be retrieved as a stranger, missing permission")
71 | }
72 | return nil
73 | }
74 |
75 | func (guh *getUserHandler) response(ent *model.UserEntity) (*charonrpc.GetUserResponse, error) {
76 | msg, err := mapping.ReverseUser(ent)
77 | if err != nil {
78 | return nil, grpcerr.E(codes.Internal, "user entity mapping failure", err)
79 | }
80 | return &charonrpc.GetUserResponse{
81 | User: msg,
82 | }, nil
83 | }
84 |
--------------------------------------------------------------------------------
/internal/charond/handler_login.go:
--------------------------------------------------------------------------------
1 | package charond
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | "github.com/piotrkowalczuk/charon/internal/service"
8 | "go.uber.org/zap"
9 |
10 | "github.com/golang/protobuf/ptypes/wrappers"
11 | charonrpc "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1"
12 | "github.com/piotrkowalczuk/charon/internal/grpcerr"
13 | "github.com/piotrkowalczuk/charon/internal/session"
14 | "github.com/piotrkowalczuk/mnemosyne/mnemosynerpc"
15 |
16 | "google.golang.org/grpc/codes"
17 | )
18 |
19 | type loginHandler struct {
20 | *handler
21 | userFinderFactory *service.UserFinderFactory
22 | }
23 |
24 | // Login implements charonrpc AuthClient interface.
25 | func (lh *loginHandler) Login(ctx context.Context, r *charonrpc.LoginRequest) (*wrappers.StringValue, error) {
26 | if r.GetUsername() != "" || r.GetPassword() != "" {
27 | r.Strategy = &charonrpc.LoginRequest_UsernameAndPassword{
28 | UsernameAndPassword: &charonrpc.UsernameAndPasswordStrategy{
29 | Username: r.GetUsername(),
30 | Password: r.GetPassword(),
31 | },
32 | }
33 | }
34 |
35 | var (
36 | userFinder service.UserFinder
37 | refreshToken string
38 | )
39 | switch str := r.GetStrategy().(type) {
40 | case *charonrpc.LoginRequest_UsernameAndPassword:
41 | userFinder = lh.userFinderFactory.ByUsernameAndPassword(
42 | str.UsernameAndPassword.GetUsername(),
43 | str.UsernameAndPassword.GetPassword(),
44 | )
45 | case *charonrpc.LoginRequest_RefreshToken:
46 | refreshToken = str.RefreshToken.GetRefreshToken()
47 | userFinder = lh.userFinderFactory.ByRefreshToken(refreshToken)
48 | }
49 |
50 | usr, err := userFinder.FindUser(ctx)
51 | if err != nil {
52 | fmt.Println("error", err)
53 | return nil, err
54 | }
55 |
56 | if !usr.IsConfirmed {
57 | return nil, grpcerr.E(codes.Unauthenticated, "user is not confirmed")
58 | }
59 |
60 | if !usr.IsActive {
61 | return nil, grpcerr.E(codes.Unauthenticated, "user is not active")
62 | }
63 |
64 | res, err := lh.session.Start(ctx, &mnemosynerpc.StartRequest{
65 | Session: &mnemosynerpc.Session{
66 | SubjectId: session.ActorIDFromInt64(usr.ID).String(),
67 | SubjectClient: r.Client,
68 | RefreshToken: refreshToken,
69 | Bag: map[string]string{
70 | "username": usr.Username,
71 | "first_name": usr.FirstName,
72 | "last_name": usr.LastName,
73 | },
74 | },
75 | })
76 | if err != nil {
77 | return nil, grpcerr.E("session start on login failure", err)
78 | }
79 |
80 | lh.logger.Debug("user session has been started", zap.Int64("user_id", usr.ID))
81 |
82 | _, err = lh.repository.user.UpdateLastLoginAt(ctx, usr.ID)
83 | if err != nil {
84 | return nil, grpcerr.E(codes.Internal, "last login update failure: %s", err)
85 | }
86 |
87 | lh.logger.Debug("user last login at field has been updated", zap.Int64("user_id", usr.ID))
88 |
89 | return &wrappers.StringValue{Value: res.Session.AccessToken}, nil
90 | }
91 |
--------------------------------------------------------------------------------
/cmd/charonctl/config.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "os"
7 |
8 | "time"
9 |
10 | "github.com/piotrkowalczuk/charon"
11 | )
12 |
13 | type configuration struct {
14 | cl *flag.FlagSet
15 | address string
16 | auth struct {
17 | username string
18 | password string
19 | enabled bool
20 | }
21 | register struct {
22 | ifNotExists bool
23 | username string
24 | password string
25 | firstName string
26 | lastName string
27 | superuser bool
28 | confirmed bool
29 | staff bool
30 | active bool
31 | permissions charon.Permissions
32 | }
33 | refreshToken struct {
34 | expireAfter time.Duration
35 | notes string
36 | }
37 | fixtures struct {
38 | path string
39 | }
40 | }
41 |
42 | func (c *configuration) init() {
43 | *c = configuration{
44 | cl: flag.NewFlagSet(os.Args[0], flag.ExitOnError),
45 | }
46 |
47 | c.cl.Usage = func() {
48 | fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
49 | c.cl.PrintDefaults()
50 | }
51 | c.cl.StringVar(&c.address, "address", "charond:8080", "charon address")
52 | c.cl.BoolVar(&c.auth.enabled, "auth", true, "authorization check flag")
53 | c.cl.StringVar(&c.auth.username, "auth.username", "", "username")
54 | c.cl.StringVar(&c.auth.password, "auth.password", "", "password")
55 | // register
56 | c.cl.BoolVar(&c.register.ifNotExists, "register.ifnotexists", false, "application does not fail if user already exists")
57 | c.cl.StringVar(&c.register.username, "register.username", "", "username")
58 | c.cl.StringVar(&c.register.password, "register.password", "", "password")
59 | c.cl.StringVar(&c.register.firstName, "register.firstname", "", "first name")
60 | c.cl.StringVar(&c.register.lastName, "register.lastname", "", "last name")
61 | c.cl.Var(&c.register.permissions, "register.permission", "list of permissions that user should")
62 | c.cl.BoolVar(&c.register.superuser, "register.superuser", false, "is user the superuser")
63 | c.cl.BoolVar(&c.register.confirmed, "register.confirmed", false, "is user account confirmed")
64 | c.cl.BoolVar(&c.register.staff, "register.staff", false, "is user part of the staff")
65 | c.cl.BoolVar(&c.register.active, "register.active", false, "is user account active")
66 | // refresh token
67 | c.cl.DurationVar(&c.refreshToken.expireAfter, "refreshtoken.expireafter", 0, "duration after which token expires")
68 | c.cl.StringVar(&c.refreshToken.notes, "refreshtoken.notes", "", "extra notes")
69 | // fixtures
70 | c.cl.StringVar(&c.fixtures.path, "fixtures.path", "", "path to the fixtures path")
71 |
72 | }
73 |
74 | func (c *configuration) parse() {
75 | if c == nil || c.cl == nil {
76 | c.init()
77 | }
78 | if !c.cl.Parsed() {
79 | if len(os.Args) > 1 {
80 | c.cl.Parse(os.Args[2:])
81 | }
82 | }
83 | }
84 |
85 | func (c *configuration) cmd() string {
86 | if len(os.Args) > 1 {
87 | return os.Args[1]
88 | }
89 | return "help"
90 | }
91 |
--------------------------------------------------------------------------------
/internal/model/modelmock/rows.go:
--------------------------------------------------------------------------------
1 | // Code generated by mockery v1.0.0. DO NOT EDIT.
2 |
3 | package modelmock
4 |
5 | import mock "github.com/stretchr/testify/mock"
6 |
7 | import sql "database/sql"
8 |
9 | // Rows is an autogenerated mock type for the Rows type
10 | type Rows struct {
11 | mock.Mock
12 | }
13 |
14 | // Close provides a mock function with given fields:
15 | func (_m *Rows) Close() error {
16 | ret := _m.Called()
17 |
18 | var r0 error
19 | if rf, ok := ret.Get(0).(func() error); ok {
20 | r0 = rf()
21 | } else {
22 | r0 = ret.Error(0)
23 | }
24 |
25 | return r0
26 | }
27 |
28 | // ColumnTypes provides a mock function with given fields:
29 | func (_m *Rows) ColumnTypes() ([]*sql.ColumnType, error) {
30 | ret := _m.Called()
31 |
32 | var r0 []*sql.ColumnType
33 | if rf, ok := ret.Get(0).(func() []*sql.ColumnType); ok {
34 | r0 = rf()
35 | } else {
36 | if ret.Get(0) != nil {
37 | r0 = ret.Get(0).([]*sql.ColumnType)
38 | }
39 | }
40 |
41 | var r1 error
42 | if rf, ok := ret.Get(1).(func() error); ok {
43 | r1 = rf()
44 | } else {
45 | r1 = ret.Error(1)
46 | }
47 |
48 | return r0, r1
49 | }
50 |
51 | // Columns provides a mock function with given fields:
52 | func (_m *Rows) Columns() ([]string, error) {
53 | ret := _m.Called()
54 |
55 | var r0 []string
56 | if rf, ok := ret.Get(0).(func() []string); ok {
57 | r0 = rf()
58 | } else {
59 | if ret.Get(0) != nil {
60 | r0 = ret.Get(0).([]string)
61 | }
62 | }
63 |
64 | var r1 error
65 | if rf, ok := ret.Get(1).(func() error); ok {
66 | r1 = rf()
67 | } else {
68 | r1 = ret.Error(1)
69 | }
70 |
71 | return r0, r1
72 | }
73 |
74 | // Err provides a mock function with given fields:
75 | func (_m *Rows) Err() error {
76 | ret := _m.Called()
77 |
78 | var r0 error
79 | if rf, ok := ret.Get(0).(func() error); ok {
80 | r0 = rf()
81 | } else {
82 | r0 = ret.Error(0)
83 | }
84 |
85 | return r0
86 | }
87 |
88 | // Next provides a mock function with given fields:
89 | func (_m *Rows) Next() bool {
90 | ret := _m.Called()
91 |
92 | var r0 bool
93 | if rf, ok := ret.Get(0).(func() bool); ok {
94 | r0 = rf()
95 | } else {
96 | r0 = ret.Get(0).(bool)
97 | }
98 |
99 | return r0
100 | }
101 |
102 | // NextResultSet provides a mock function with given fields:
103 | func (_m *Rows) NextResultSet() bool {
104 | ret := _m.Called()
105 |
106 | var r0 bool
107 | if rf, ok := ret.Get(0).(func() bool); ok {
108 | r0 = rf()
109 | } else {
110 | r0 = ret.Get(0).(bool)
111 | }
112 |
113 | return r0
114 | }
115 |
116 | // Scan provides a mock function with given fields: dst
117 | func (_m *Rows) Scan(dst ...interface{}) error {
118 | var _ca []interface{}
119 | _ca = append(_ca, dst...)
120 | ret := _m.Called(_ca...)
121 |
122 | var r0 error
123 | if rf, ok := ret.Get(0).(func(...interface{}) error); ok {
124 | r0 = rf(dst...)
125 | } else {
126 | r0 = ret.Error(0)
127 | }
128 |
129 | return r0
130 | }
131 |
--------------------------------------------------------------------------------
/scripts/docker-entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -e
3 |
4 | : ${CHAROND_PORT:=8080}
5 | : ${CHAROND_HOST:=0.0.0.0}
6 | : ${MNEMOSYNED_LOG_ENVIRONMENT:=production}
7 | : ${MNEMOSYNED_LOG_LEVEL:=info}
8 | : ${CHAROND_STORAGE:=postgres}
9 | : ${CHAROND_MONITORING:=false}
10 | : ${CHAROND_PASSWORD_BCRYPT_COST:=10}
11 | : ${CHAROND_MNEMOSYNED_ADDRESS:=mnemosyned:8080}
12 | : ${CHAROND_MNEMOSYNED_TLS_ENABLED:=false}
13 | : ${CHAROND_POSTGRES_ADDRESS:=postgres://postgres:postgres@postgres/postgres?sslmode=disable}
14 | : ${CHAROND_POSTGRES_DEBUG:=false}
15 | : ${CHAROND_TLS_ENABLED:=false}
16 |
17 | if [ "$1" = 'charond' ]; then
18 | exec charond \
19 | -host=${CHAROND_HOST} \
20 | -port=${CHAROND_PORT} \
21 | -log.environment=${CHAROND_LOG_ENVIRONMENT} \
22 | -log.level=${CHAROND_LOG_LEVEL} \
23 | -mnemosyned.address=${CHAROND_MNEMOSYNED_ADDRESS} \
24 | -mnemosyned.tls=${CHAROND_MNEMOSYNED_TLS_ENABLED} \
25 | -mnemosyned.tls.crt=${CHAROND_MNEMOSYNED_TLS_CRT} \
26 | -password.strategy=${CHAROND_PASSWORD_STRATEGY} \
27 | -password.bcryptcost=${CHAROND_PASSWORD_BCRYPT_COST} \
28 | -monitoring=${CHAROND_MONITORING} \
29 | -postgres.address=${CHAROND_POSTGRES_ADDRESS} \
30 | -postgres.debug=${CHAROND_POSTGRES_DEBUG} \
31 | -tls=${CHAROND_TLS_ENABLED} \
32 | -tls.crt=${CHAROND_TLS_CRT} \
33 | -tls.key=${CHAROND_TLS_KEY}
34 | fi
35 |
36 | : ${CHARONCTL_CHAROND_HOST:=charond}
37 | : ${CHARONCTL_AUTH_ENABLED:=true}
38 | : ${CHARONCTL_REGISTER_SUPERUSER:=false}
39 | : ${CHARONCTL_REGISTER_CONFIRMED:=false}
40 | : ${CHARONCTL_REGISTER_STAFF:=false}
41 | : ${CHARONCTL_REGISTER_ACTIVE:=false}
42 | : ${CHARONCTL_REGISTER_IF_NOT_EXISTS:=false}
43 | : ${CHARONCTL_REGISTER_PERMISSIONS:=""}
44 | : ${CHARONCTL_FIXTURES_PATH:="/data/fixtures.json"}
45 |
46 | if [ "$1" = 'charonctl' ]; then
47 | if [ "$2" = 'register' ]; then
48 | eval charonctl register \
49 | -address='${CHARONCTL_CHAROND_HOST}:${CHAROND_PORT}' \
50 | -auth=${CHARONCTL_AUTH_ENABLED} \
51 | -auth.username=\"${CHARONCTL_AUTH_USERNAME}\" \
52 | -auth.password=\"${CHARONCTL_AUTH_PASSWORD}\" \
53 | -register.ifnotexists=${CHARONCTL_REGISTER_IF_NOT_EXISTS} \
54 | -register.username=\"${CHARONCTL_REGISTER_USERNAME}\" \
55 | -register.password=\"${CHARONCTL_REGISTER_PASSWORD}\" \
56 | -register.firstname=\"${CHARONCTL_REGISTER_FIRSTNAME}\" \
57 | -register.lastname=\"${CHARONCTL_REGISTER_LASTNAME}\" \
58 | -register.superuser=${CHARONCTL_REGISTER_SUPERUSER} \
59 | -register.confirmed=${CHARONCTL_REGISTER_CONFIRMED} \
60 | -register.staff=${CHARONCTL_REGISTER_STAFF} \
61 | -register.active=${CHARONCTL_REGISTER_ACTIVE} \
62 | -register.permission=\"${CHARONCTL_REGISTER_PERMISSIONS}\"
63 | exit $?
64 | fi
65 | if [ "$2" = 'load' ]; then
66 | eval charonctl load \
67 | -address='${CHARONCTL_CHAROND_HOST}:${CHAROND_PORT}' \
68 | -auth=${CHARONCTL_AUTH_ENABLED} \
69 | -auth.username=\"${CHARONCTL_AUTH_USERNAME}\" \
70 | -auth.password=\"${CHARONCTL_AUTH_PASSWORD}\" \
71 | -fixtures.path=\"${CHARONCTL_FIXTURES_PATH}\"
72 | exit $?
73 | fi
74 | fi
75 |
76 | exec "$@"
77 |
--------------------------------------------------------------------------------
/internal/session/actor_provider.go:
--------------------------------------------------------------------------------
1 | package session
2 |
3 | import (
4 | "context"
5 | "database/sql"
6 | "strings"
7 |
8 | "github.com/golang/protobuf/ptypes/empty"
9 | "github.com/piotrkowalczuk/charon"
10 | "github.com/piotrkowalczuk/charon/internal/grpcerr"
11 | "github.com/piotrkowalczuk/charon/internal/model"
12 | "github.com/piotrkowalczuk/mnemosyne/mnemosynerpc"
13 | "google.golang.org/grpc/codes"
14 | "google.golang.org/grpc/metadata"
15 | "google.golang.org/grpc/peer"
16 | "google.golang.org/grpc/status"
17 | )
18 |
19 | type ActorProvider interface {
20 | Actor(context.Context) (*Actor, error)
21 | }
22 |
23 | type MnemosyneActorProvider struct {
24 | Client mnemosynerpc.SessionManagerClient
25 | UserProvider model.UserProvider
26 | PermissionProvider model.PermissionProvider
27 | }
28 |
29 | func (p *MnemosyneActorProvider) Actor(ctx context.Context) (*Actor, error) {
30 | var (
31 | act *Actor
32 | userID int64
33 | entities []*model.PermissionEntity
34 | res *mnemosynerpc.ContextResponse
35 | )
36 |
37 | res, err := p.Client.Context(ctx, &empty.Empty{})
38 | if err != nil {
39 | if isLocal(ctx) {
40 | return &Actor{
41 | User: &model.UserEntity{},
42 | IsLocal: true,
43 | }, nil
44 | }
45 | return nil, handleMnemosyneError(err)
46 | }
47 |
48 | userID, err = ActorID(res.Session.SubjectId).UserID()
49 | if err != nil {
50 | return nil, grpcerr.E(codes.InvalidArgument, err)
51 | }
52 |
53 | act = &Actor{}
54 | act.User, err = p.UserProvider.FindOneByID(ctx, userID)
55 | if err != nil {
56 | if err == sql.ErrNoRows {
57 | return nil, grpcerr.E(codes.PermissionDenied, "actor does not exists")
58 | }
59 | return nil, grpcerr.E(codes.Internal, "actor fetch failure", err)
60 | }
61 | entities, err = p.PermissionProvider.FindByUserID(ctx, userID)
62 | if err != nil {
63 | if err == sql.ErrNoRows {
64 | return act, nil
65 | }
66 | return nil, grpcerr.E(codes.Internal, "permissions fetch failure", err)
67 | }
68 |
69 | act.Permissions = make(charon.Permissions, 0, len(entities))
70 | for _, e := range entities {
71 | act.Permissions = append(act.Permissions, e.Permission())
72 | }
73 |
74 | return act, nil
75 | }
76 |
77 | func isLocal(ctx context.Context) bool {
78 | if p, ok := peer.FromContext(ctx); ok {
79 | if strings.HasPrefix(p.Addr.String(), "127.0.0.1") {
80 | if md, ok := metadata.FromIncomingContext(ctx); ok {
81 | if len(md["user-agent"]) == 1 && strings.HasPrefix(md["user-agent"][0], "charonctl") {
82 | return true
83 | }
84 | }
85 | }
86 | }
87 | return false
88 | }
89 |
90 | func handleMnemosyneError(err error) error {
91 | if sts, ok := status.FromError(err); ok {
92 | switch sts.Code() {
93 | case codes.NotFound:
94 | return grpcerr.E(codes.Unauthenticated, "session not found")
95 | case codes.InvalidArgument:
96 | return grpcerr.E(codes.Unauthenticated, sts.Message())
97 | }
98 | }
99 |
100 | return grpcerr.E(codes.Internal, "session fetch failure", err)
101 | }
102 |
--------------------------------------------------------------------------------
/internal/charond/service.go:
--------------------------------------------------------------------------------
1 | package charond
2 |
3 | import (
4 | "context"
5 | "database/sql"
6 | "fmt"
7 | "net/url"
8 | "time"
9 |
10 | "github.com/piotrkowalczuk/charon"
11 | "github.com/piotrkowalczuk/charon/internal/model"
12 | "github.com/piotrkowalczuk/charon/internal/password"
13 | "github.com/piotrkowalczuk/mnemosyne/mnemosynerpc"
14 | "go.uber.org/zap"
15 | "google.golang.org/grpc"
16 | )
17 |
18 | func initPostgres(address string, test bool, logger *zap.Logger) (*sql.DB, error) {
19 | db, err := sql.Open("postgres", address)
20 | if err != nil {
21 | return nil, fmt.Errorf("connection failure: %s", err.Error())
22 | }
23 |
24 | if err = db.Ping(); err != nil {
25 | cancel := time.NewTimer(10 * time.Second)
26 | attempts := 1
27 | PingLoop:
28 | for {
29 | select {
30 | case <-time.After(1 * time.Second):
31 | if err := db.Ping(); err != nil {
32 | attempts++
33 | continue PingLoop
34 | }
35 | break PingLoop
36 | case <-cancel.C:
37 | return nil, fmt.Errorf("postgres connection failed after %d attempts", attempts)
38 | }
39 | }
40 | }
41 |
42 | if test {
43 | if err = teardownDatabase(db); err != nil {
44 | return nil, err
45 | }
46 | logger.Info("database has been cleared upfront")
47 | }
48 | if err = setupDatabase(db); err != nil {
49 | return nil, err
50 | }
51 |
52 | u, err := url.Parse(address)
53 | if err != nil {
54 | return nil, err
55 | }
56 | username := ""
57 | if u.User != nil {
58 | username = u.User.Username()
59 | }
60 |
61 | logger.Info("postgres connection has been established", zap.String("host", u.Host), zap.String("username", username))
62 |
63 | return db, nil
64 | }
65 |
66 | func initMnemosyne(address string, logger *zap.Logger, opts []grpc.DialOption) (mnemosynerpc.SessionManagerClient, *grpc.ClientConn) {
67 | if address == "" {
68 | logger.Error("missing mnemosyne address")
69 | }
70 | conn, err := grpc.Dial(address, opts...)
71 | if err != nil {
72 | logger.Error("mnemosyne dial falilure", zap.Error(err), zap.String("address", address))
73 | }
74 |
75 | logger.Info("rpc connection to mnemosyne has been established", zap.String("address", address))
76 |
77 | return mnemosynerpc.NewSessionManagerClient(conn), conn
78 | }
79 |
80 | func initHasher(cost int, logger *zap.Logger) password.Hasher {
81 | bh, err := password.NewBCryptHasher(cost)
82 | if err != nil {
83 | logger.Fatal("hasher initialization failure", zap.Error(err))
84 | }
85 |
86 | return bh
87 | }
88 |
89 | func initPermissionRegistry(r model.PermissionProvider, permissions charon.Permissions, logger *zap.Logger) (pr model.PermissionRegistry) {
90 | pr = model.NewPermissionRegistry(r)
91 | created, untouched, removed, err := pr.Register(context.TODO(), permissions)
92 | if err != nil {
93 | logger.Fatal("permission registry initialization failure", zap.Error(err))
94 | }
95 |
96 | logger.Info("charon permissions has been registered", zap.Int64("created", created), zap.Int64("untouched", untouched), zap.Int64("removed", removed))
97 |
98 | return
99 | }
100 |
--------------------------------------------------------------------------------
/internal/charond/handler_create_refresh_token.go:
--------------------------------------------------------------------------------
1 | package charond
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/golang/protobuf/ptypes"
7 | "github.com/lib/pq"
8 | "github.com/piotrkowalczuk/charon"
9 | charonrpc "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1"
10 | "github.com/piotrkowalczuk/charon/internal/grpcerr"
11 | "github.com/piotrkowalczuk/charon/internal/mapping"
12 | "github.com/piotrkowalczuk/charon/internal/model"
13 | "github.com/piotrkowalczuk/charon/internal/refreshtoken"
14 | "github.com/piotrkowalczuk/charon/internal/session"
15 | "github.com/piotrkowalczuk/ntypes"
16 | "golang.org/x/net/context"
17 | "google.golang.org/grpc/codes"
18 | )
19 |
20 | type createRefreshTokenHandler struct {
21 | *handler
22 | }
23 |
24 | func (crth *createRefreshTokenHandler) Create(ctx context.Context, req *charonrpc.CreateRefreshTokenRequest) (*charonrpc.CreateRefreshTokenResponse, error) {
25 | var (
26 | expireAt time.Time
27 | err error
28 | )
29 | if req.ExpireAt != nil {
30 | if expireAt, err = ptypes.Timestamp(req.ExpireAt); err != nil {
31 | return nil, grpcerr.E(codes.InvalidArgument, "invalid format of expire at", err)
32 | }
33 | }
34 |
35 | act, err := crth.Actor(ctx)
36 | if err != nil {
37 | return nil, err
38 | }
39 | if err = crth.firewall(req, act); err != nil {
40 | return nil, err
41 | }
42 |
43 | tkn, err := refreshtoken.Random()
44 | if err != nil {
45 | return nil, grpcerr.E(codes.Internal, "refresh token generation failure", err)
46 | }
47 |
48 | ent, err := crth.repository.refreshToken.Create(ctx, &model.RefreshTokenEntity{
49 | UserID: act.User.ID,
50 | Token: tkn,
51 | ExpireAt: pq.NullTime{
52 | Time: expireAt.UTC(),
53 | Valid: !expireAt.IsZero(),
54 | },
55 | CreatedBy: ntypes.Int64{Int64: act.User.ID, Valid: true},
56 | Notes: allocNilString(req.Notes),
57 | })
58 | if err != nil {
59 | switch model.ErrorConstraint(err) {
60 | case model.TableRefreshTokenConstraintCreatedByForeignKey:
61 | return nil, grpcerr.E(codes.NotFound, "such user does not exist")
62 | case model.TableRefreshTokenConstraintTokenUnique:
63 | return nil, grpcerr.E(codes.AlreadyExists, "such refresh token already exists")
64 | case model.TableRefreshTokenConstraintUserIDForeignKey:
65 | return nil, grpcerr.E(codes.NotFound, "such user does not exist")
66 | default:
67 | return nil, grpcerr.E(codes.Internal, "refresh token persistence failure", err)
68 | }
69 | }
70 |
71 | return crth.response(ent)
72 | }
73 |
74 | func (crth *createRefreshTokenHandler) firewall(req *charonrpc.CreateRefreshTokenRequest, act *session.Actor) error {
75 | if act.User.IsSuperuser {
76 | return nil
77 | }
78 | if act.Permissions.Contains(charon.RefreshTokenCanCreate) {
79 | return nil
80 | }
81 |
82 | return grpcerr.E(codes.PermissionDenied, "refresh token cannot be created, missing permission")
83 | }
84 |
85 | func (crth *createRefreshTokenHandler) response(ent *model.RefreshTokenEntity) (*charonrpc.CreateRefreshTokenResponse, error) {
86 | msg, err := mapping.ReverseRefreshToken(ent)
87 | if err != nil {
88 | return nil, grpcerr.E(codes.Internal, "refresh token entity mapping failure", err)
89 | }
90 | return &charonrpc.CreateRefreshTokenResponse{
91 | RefreshToken: msg,
92 | }, nil
93 | }
94 |
--------------------------------------------------------------------------------
/internal/charond/handler_delete_user.go:
--------------------------------------------------------------------------------
1 | package charond
2 |
3 | import (
4 | "context"
5 | "database/sql"
6 |
7 | "github.com/golang/protobuf/ptypes/wrappers"
8 | "github.com/piotrkowalczuk/charon"
9 | charonrpc "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1"
10 | "github.com/piotrkowalczuk/charon/internal/grpcerr"
11 | "github.com/piotrkowalczuk/charon/internal/model"
12 | "github.com/piotrkowalczuk/charon/internal/session"
13 |
14 | "google.golang.org/grpc/codes"
15 | )
16 |
17 | type deleteUserHandler struct {
18 | *handler
19 | }
20 |
21 | func (duh *deleteUserHandler) Delete(ctx context.Context, req *charonrpc.DeleteUserRequest) (*wrappers.BoolValue, error) {
22 | if req.Id <= 0 {
23 | return nil, grpcerr.E(codes.InvalidArgument, "user cannot be deleted, invalid id")
24 | }
25 |
26 | act, err := duh.Actor(ctx)
27 | if err != nil {
28 | return nil, err
29 | }
30 | ent, err := duh.repository.user.FindOneByID(ctx, req.Id)
31 | if err != nil {
32 | if err == sql.ErrNoRows {
33 | return nil, grpcerr.E(codes.NotFound, "user does not exists")
34 | }
35 | return nil, grpcerr.E(codes.Internal, "user retrieval failure", err)
36 | }
37 | if err = duh.firewall(req, act, ent); err != nil {
38 | return nil, err
39 | }
40 |
41 | aff, err := duh.repository.user.DeleteOneByID(ctx, req.Id)
42 | if err != nil {
43 | switch model.ErrorConstraint(err) {
44 | case model.TableUserGroupsConstraintUserIDForeignKey:
45 | return nil, grpcerr.E(codes.FailedPrecondition, "user cannot be removed, groups are assigned to it")
46 | case model.TableUserPermissionsConstraintUserIDForeignKey:
47 | return nil, grpcerr.E(codes.FailedPrecondition, "user cannot be removed, permissions are assigned to it")
48 | default:
49 | return nil, grpcerr.E(codes.Internal, "user cannot be removed", err)
50 | }
51 | }
52 |
53 | return &wrappers.BoolValue{Value: aff > 0}, nil
54 | }
55 |
56 | func (duh *deleteUserHandler) firewall(req *charonrpc.DeleteUserRequest, act *session.Actor, ent *model.UserEntity) error {
57 | if act.User.ID == ent.ID {
58 | return grpcerr.E(codes.PermissionDenied, "user is not permitted to remove himself")
59 | }
60 | if act.User.IsSuperuser {
61 | return nil
62 | }
63 | if ent.IsSuperuser {
64 | return grpcerr.E(codes.PermissionDenied, "only superuser can remove other superuser")
65 | }
66 | if ent.IsStaff {
67 | switch {
68 | case act.User.ID == ent.CreatedBy.Int64Or(0):
69 | if !act.Permissions.Contains(charon.UserCanDeleteStaffAsOwner) {
70 | return grpcerr.E(codes.PermissionDenied, "staff user cannot be removed by owner, missing permission")
71 | }
72 | return nil
73 | case !act.Permissions.Contains(charon.UserCanDeleteStaffAsStranger):
74 | return grpcerr.E(codes.PermissionDenied, "staff user cannot be removed by stranger, missing permission")
75 | }
76 | return nil
77 | }
78 |
79 | if act.User.ID == ent.CreatedBy.Int64Or(0) {
80 | if !act.Permissions.Contains(charon.UserCanDeleteAsOwner) {
81 | return grpcerr.E(codes.PermissionDenied, "user cannot be removed by owner, missing permission")
82 | }
83 | return nil
84 | }
85 | if !act.Permissions.Contains(charon.UserCanDeleteAsStranger) {
86 | return grpcerr.E(codes.PermissionDenied, "user cannot be removed by stranger, missing permission")
87 | }
88 | return nil
89 | }
90 |
--------------------------------------------------------------------------------
/internal/service/user_finder.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "database/sql"
7 |
8 | "github.com/piotrkowalczuk/charon/internal/password"
9 |
10 | "github.com/piotrkowalczuk/charon/internal/grpcerr"
11 | "github.com/piotrkowalczuk/charon/internal/model"
12 | "google.golang.org/grpc/codes"
13 | )
14 |
15 | type UserFinder interface {
16 | FindUser(context.Context) (*model.UserEntity, error)
17 | }
18 |
19 | type UserFinderFactory struct {
20 | UserRepository model.UserProvider
21 | RefreshTokenRepository model.RefreshTokenProvider
22 | Hasher password.Hasher
23 | }
24 |
25 | func (f *UserFinderFactory) ByUsernameAndPassword(username, password string) UserFinder {
26 | return &byUsernameAndPasswordUserFinder{
27 | username: username,
28 | password: password,
29 | userRepository: f.UserRepository,
30 | hasher: f.Hasher,
31 | }
32 | }
33 |
34 | func (f *UserFinderFactory) ByRefreshToken(refreshToken string) UserFinder {
35 | return &byRefreshTokenUserFinder{
36 | refreshToken: refreshToken,
37 | userRepository: f.UserRepository,
38 | refreshTokenRepository: f.RefreshTokenRepository,
39 | }
40 | }
41 |
42 | type byUsernameAndPasswordUserFinder struct {
43 | username, password string
44 | userRepository model.UserProvider
45 | hasher password.Hasher
46 | }
47 |
48 | var _ UserFinder = &byUsernameAndPasswordUserFinder{}
49 |
50 | func (f *byUsernameAndPasswordUserFinder) FindUser(ctx context.Context) (*model.UserEntity, error) {
51 | if f.username == "" {
52 | return nil, grpcerr.E(codes.InvalidArgument, "empty username")
53 | }
54 | if len(f.password) == 0 {
55 | return nil, grpcerr.E(codes.InvalidArgument, "empty password")
56 | }
57 |
58 | usr, err := f.userRepository.FindOneByUsername(ctx, f.username)
59 | if err != nil {
60 | if err == sql.ErrNoRows {
61 | return nil, grpcerr.E(codes.Unauthenticated, "user with such username or password does not exists")
62 | }
63 | return nil, err
64 | }
65 |
66 | if bytes.Equal(usr.Password, model.ExternalPassword) {
67 | return nil, grpcerr.E(codes.FailedPrecondition, "authentication failure, external password manager not implemented")
68 | }
69 | if matches := f.hasher.Compare(usr.Password, []byte(f.password)); !matches {
70 | return nil, grpcerr.E(codes.Unauthenticated, "user with such username or password does not exists")
71 | }
72 |
73 | return usr, nil
74 | }
75 |
76 | type byRefreshTokenUserFinder struct {
77 | refreshToken string
78 | userRepository model.UserProvider
79 | refreshTokenRepository model.RefreshTokenProvider
80 | }
81 |
82 | var _ UserFinder = &byRefreshTokenUserFinder{}
83 |
84 | func (f *byRefreshTokenUserFinder) FindUser(ctx context.Context) (*model.UserEntity, error) {
85 | if f.refreshToken == "" {
86 | return nil, grpcerr.E(codes.InvalidArgument, "empty refresh token")
87 | }
88 |
89 | refreshToken, err := f.refreshTokenRepository.FindOneByToken(ctx, f.refreshToken)
90 | if err != nil {
91 | return nil, err
92 | }
93 | user, err := f.userRepository.FindOneByID(ctx, refreshToken.UserID)
94 | if err != nil {
95 | return nil, err
96 | }
97 |
98 | return user, nil
99 | }
100 |
--------------------------------------------------------------------------------
/internal/charond/handler_revoke_refresh_token.go:
--------------------------------------------------------------------------------
1 | package charond
2 |
3 | import (
4 | "database/sql"
5 |
6 | "github.com/piotrkowalczuk/charon"
7 | charonrpc "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1"
8 | "github.com/piotrkowalczuk/charon/internal/grpcerr"
9 | "github.com/piotrkowalczuk/charon/internal/mapping"
10 | "github.com/piotrkowalczuk/charon/internal/model"
11 | "github.com/piotrkowalczuk/charon/internal/session"
12 | "github.com/piotrkowalczuk/mnemosyne/mnemosynerpc"
13 | "github.com/piotrkowalczuk/ntypes"
14 | "go.uber.org/zap"
15 | "golang.org/x/net/context"
16 | "google.golang.org/grpc/codes"
17 | "google.golang.org/grpc/status"
18 | )
19 |
20 | type revokeRefreshTokenHandler struct {
21 | *handler
22 | }
23 |
24 | func (h *revokeRefreshTokenHandler) Revoke(ctx context.Context, req *charonrpc.RevokeRefreshTokenRequest) (*charonrpc.RevokeRefreshTokenResponse, error) {
25 | if len(req.Token) == 0 {
26 | return nil, grpcerr.E(codes.InvalidArgument, "refresh token cannot be disabled, invalid token: %s", req.Token)
27 | }
28 | if req.UserId == 0 {
29 | return nil, grpcerr.E(codes.InvalidArgument, "refresh token cannot be disabled, missing user id")
30 | }
31 |
32 | act, err := h.Actor(ctx)
33 | if err != nil {
34 | return nil, err
35 | }
36 | ent, err := h.repository.refreshToken.FindOneByTokenAndUserID(ctx, req.Token, req.UserId)
37 | if err != nil {
38 | if err == sql.ErrNoRows {
39 | return nil, grpcerr.E(codes.NotFound, "refresh token does not exists")
40 | }
41 | return nil, grpcerr.E(codes.Internal, "refresh token could not be retrieved", err)
42 | }
43 | if err = h.firewall(req, act, ent); err != nil {
44 | return nil, err
45 | }
46 |
47 | ent, err = h.repository.refreshToken.UpdateOneByToken(ctx, req.Token, &model.RefreshTokenPatch{
48 | Revoked: ntypes.Bool{Bool: true, Valid: true},
49 | })
50 | if err != nil {
51 | if err == sql.ErrNoRows {
52 | return nil, grpcerr.E(codes.NotFound, "refresh token does not exists")
53 | }
54 | return nil, grpcerr.E(codes.Internal, "refresh token could not be disabled", err)
55 | }
56 |
57 | res, err := h.session.Delete(ctx, &mnemosynerpc.DeleteRequest{
58 | SubjectId: session.ActorIDFromInt64(ent.UserID).String(),
59 | RefreshToken: req.Token,
60 | })
61 | if err != nil {
62 | if status.Code(err) != codes.NotFound {
63 | return nil, grpcerr.E(codes.Internal, "session could not be removed", err)
64 | }
65 | }
66 | h.logger.Debug("refresh token corresponding sessions removed", zap.Int64("count", res.Value))
67 |
68 | msg, err := mapping.ReverseRefreshToken(ent)
69 | if err != nil {
70 | return nil, grpcerr.E(codes.Internal, "refresh token mapping failure", err)
71 | }
72 | return &charonrpc.RevokeRefreshTokenResponse{
73 | RefreshToken: msg,
74 | }, nil
75 | }
76 |
77 | func (h *revokeRefreshTokenHandler) firewall(req *charonrpc.RevokeRefreshTokenRequest, act *session.Actor, ent *model.RefreshTokenEntity) error {
78 | if act.User.IsSuperuser {
79 | return nil
80 | }
81 | if act.Permissions.Contains(charon.RefreshTokenCanRevokeAsStranger) {
82 | return nil
83 | }
84 | if act.Permissions.Contains(charon.RefreshTokenCanRevokeAsOwner) {
85 | if act.User.ID == ent.UserID {
86 | return nil
87 | }
88 | return grpcerr.E(codes.PermissionDenied, "refresh token cannot be revoked by stranger, missing permission")
89 | }
90 | return grpcerr.E(codes.PermissionDenied, "refresh token cannot be revoked, missing permission")
91 | }
92 |
--------------------------------------------------------------------------------
/presentation.slide:
--------------------------------------------------------------------------------
1 | #+theme=black
2 |
3 | Charon
4 | Authorization and authentication service.
5 | 20:40 26 Mar 2018
6 | Tags: authorization, authentication, service, grpc
7 |
8 | Piotr Kowalczuk
9 | p.kowalczuk.priv@gmail.com
10 | https://gofunc.pl
11 | @kowalczuk_piotr
12 |
13 | * What Charon is?
14 |
15 | Charon is authorization and authentication service written in *Go* that expose language agnostic RPC API.
16 |
17 | First commit: *Apr* *19,* *2015*
18 | Latest version: *v0.16.3*
19 |
20 | .image data/logo/charon.png _ 300
21 |
22 | * Functional requirements
23 |
24 | *Authentication*
25 |
26 | - authentication based on *access* *token*
27 | - ability to obtain access token using *username/password* and/or *refresh* *token*
28 |
29 | *Authorization*
30 |
31 | - *RBAC* - Role based access control
32 | - *ABAC* - Attribute based access control _(simplified)_
33 |
34 | *Other*
35 |
36 | - CRUD API
37 | - Backend for OAuth2 service
38 |
39 | * Technical requirements
40 |
41 | - Easy to deploy, operate and integrate (self-hosted).
42 | - Language agnostic API.
43 | - docker-compose and kubernetes ready.
44 | - Whitebox monitoring.
45 |
46 | * Available solutions
47 |
48 | none
49 |
50 | * Tech stack
51 |
52 | - Language: Go
53 | - API: RPC (using gRPC, it was not even 1.0.0 at a time)
54 | - Encoding: Protocol Buffers
55 | - Database: Postgres
56 | - Session management: Mnemosyne
57 |
58 | * Deployment
59 |
60 | .image data/deployment.png 550 _
61 |
62 | * Model
63 |
64 | - `Permission`
65 | - `RefreshToken`
66 |
67 | - `User`
68 | - `UserGroups` _(transient_ _table)_
69 | - `UserPermissions` _(transient_ _table)_
70 |
71 | - `Group`
72 | - `GroupPermissions` _(transient_ _table)_
73 |
74 | * Permissions
75 | Simple representation as a string, that can be also used as an OAuth2 scope:
76 |
77 | ::
78 |
79 | Examples:
80 |
81 | charon:user:can create
82 | news-service:comment:can modify as an owner
83 | asset-service:image:can retrieve if smaller than megabyte
84 |
85 | Management:
86 |
87 | .code pb/rpc/charond/v1/permission.pb.go /^type PermissionManagerServer interface/,/^}/
88 |
89 | * Security Context
90 |
91 | type SecurityContext interface {
92 | context.Context
93 | oauth2.TokenSource
94 | // Actor ...
95 | Actor() (Actor, bool)
96 | // AccessToken ...
97 | AccessToken() (string, bool)
98 | }
99 |
100 | * Integration
101 |
102 | func securityContext(auth *charonc.Client, ctx context.Context) (charonc.SecurityContext, error) {
103 | if sctx, ok := ctx.(charonc.SecurityContext); ok {
104 | return sctx, nil
105 | }
106 |
107 | md, ok := metadata.FromIncomingContext(ctx)
108 | if !ok {
109 | return nil, errors.New("missing metadata in context, session token cannot be retrieved")
110 | }
111 | if len(md[mnemosyne.AccessTokenMetadataKey]) == 0 {
112 | return nil, errors.New("missing session token in metadata")
113 | }
114 | act, err := auth.Actor(ctx, md[mnemosyne.AccessTokenMetadataKey][0])
115 | if err != nil {
116 | return nil, err
117 | }
118 |
119 | ctx = mnemosyne.NewAccessTokenContext(ctx, md[mnemosyne.AccessTokenMetadataKey][0])
120 | ctx = charonc.NewActorContext(ctx, *act)
121 |
122 | return charonc.NewSecurityContext(ctx), nil
123 | }
124 |
125 | * Towards v1.0.0
126 |
127 | - Refresh token functionality is not ready yet (Apps?).
128 | - `charonc` package needs to be revisited (versioning of RPC API vs public code).
129 | - Remove experimental LDAP integration.
130 | - Documentation.
131 |
132 |
133 |
134 |
135 |
136 |
--------------------------------------------------------------------------------
/internal/charond/handler_list_users.go:
--------------------------------------------------------------------------------
1 | package charond
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/piotrkowalczuk/charon"
7 | charonrpc "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1"
8 | "github.com/piotrkowalczuk/charon/internal/grpcerr"
9 | "github.com/piotrkowalczuk/charon/internal/mapping"
10 | "github.com/piotrkowalczuk/charon/internal/model"
11 | "github.com/piotrkowalczuk/charon/internal/session"
12 | "github.com/piotrkowalczuk/ntypes"
13 | "github.com/piotrkowalczuk/qtypes"
14 |
15 | "google.golang.org/grpc/codes"
16 | )
17 |
18 | type listUsersHandler struct {
19 | *handler
20 | }
21 |
22 | func (luh *listUsersHandler) List(ctx context.Context, req *charonrpc.ListUsersRequest) (*charonrpc.ListUsersResponse, error) {
23 | act, err := luh.Actor(ctx)
24 | if err != nil {
25 | return nil, err
26 | }
27 | if err = luh.firewall(req, act); err != nil {
28 | return nil, err
29 | }
30 |
31 | cri := &model.UserCriteria{
32 | IsSuperuser: allocNilBool(req.IsSuperuser),
33 | IsStaff: allocNilBool(req.IsStaff),
34 | CreatedBy: req.CreatedBy,
35 | }
36 |
37 | if !act.User.IsSuperuser {
38 | cri.IsSuperuser = *ntypes.False()
39 | }
40 | if !act.Permissions.Contains(charon.UserCanRetrieveStaffAsStranger) {
41 | cri.IsStaff = *ntypes.False()
42 | }
43 | if act.Permissions.Contains(charon.UserCanRetrieveAsOwner, charon.UserCanRetrieveStaffAsOwner) {
44 | cri.CreatedBy = qtypes.EqualInt64(act.User.ID)
45 | }
46 |
47 | ents, err := luh.repository.user.Find(ctx, &model.UserFindExpr{
48 | OrderBy: mapping.OrderBy(req.OrderBy),
49 | Offset: req.Offset.Int64Or(0),
50 | Limit: req.Limit.Int64Or(10),
51 | Where: cri,
52 | })
53 | if err != nil {
54 | return nil, grpcerr.E(codes.Internal, "find users query failed", err)
55 | }
56 | return luh.response(ents)
57 | }
58 |
59 | func (luh *listUsersHandler) firewall(req *charonrpc.ListUsersRequest, act *session.Actor) error {
60 | if act.User.IsSuperuser {
61 | return nil
62 | }
63 | if req.IsSuperuser.BoolOr(false) {
64 | return grpcerr.E(codes.PermissionDenied, "only superuser is permitted to retrieve other superusers")
65 | }
66 | // STAFF USERS
67 | if req.IsStaff.BoolOr(false) {
68 | if req.CreatedBy != nil && req.CreatedBy.Value() == act.User.ID {
69 | if !act.Permissions.Contains(charon.UserCanRetrieveStaffAsStranger, charon.UserCanRetrieveStaffAsOwner) {
70 | return grpcerr.E(codes.PermissionDenied, "list of staff users cannot be retrieved as an owner, missing permission")
71 | }
72 | return nil
73 | }
74 | if !act.Permissions.Contains(charon.UserCanRetrieveStaffAsStranger) {
75 | return grpcerr.E(codes.PermissionDenied, "list of staff users cannot be retrieved as a stranger, missing permission")
76 | }
77 | return nil
78 | }
79 | // NON STAFF USERS
80 | if req.CreatedBy != nil && req.CreatedBy.Value() == act.User.ID {
81 | if !act.Permissions.Contains(charon.UserCanRetrieveAsStranger, charon.UserCanRetrieveAsOwner) {
82 | return grpcerr.E(codes.PermissionDenied, "list of users cannot be retrieved as an owner, missing permission")
83 | }
84 | return nil
85 | }
86 | if !act.Permissions.Contains(charon.UserCanRetrieveAsStranger) {
87 | return grpcerr.E(codes.PermissionDenied, "list of users cannot be retrieved as a stranger, missing permission")
88 | }
89 | return nil
90 | }
91 |
92 | func (luh *listUsersHandler) response(ents []*model.UserEntity) (*charonrpc.ListUsersResponse, error) {
93 | msg, err := mapping.ReverseUsers(ents)
94 | if err != nil {
95 | return nil, grpcerr.E(codes.Internal, "user reverse mapping failure")
96 | }
97 | return &charonrpc.ListUsersResponse{
98 | Users: msg,
99 | }, nil
100 | }
101 |
--------------------------------------------------------------------------------
/internal/charond/handler_test.go:
--------------------------------------------------------------------------------
1 | package charond
2 |
3 | import (
4 | "context"
5 | "database/sql"
6 | "errors"
7 | "testing"
8 |
9 | "github.com/piotrkowalczuk/charon"
10 | "github.com/piotrkowalczuk/charon/internal/model"
11 | "github.com/piotrkowalczuk/charon/internal/model/modelmock"
12 | "github.com/piotrkowalczuk/charon/internal/session"
13 | "github.com/piotrkowalczuk/mnemosyne"
14 | "github.com/piotrkowalczuk/mnemosyne/mnemosynerpc"
15 | "github.com/piotrkowalczuk/mnemosyne/mnemosynetest"
16 | . "github.com/smartystreets/goconvey/convey"
17 | "github.com/stretchr/testify/mock"
18 | )
19 |
20 | func TestHandler(t *testing.T) {
21 | t.SkipNow()
22 | var (
23 | id int64
24 | ctx context.Context
25 | err error
26 | act *session.Actor
27 | tkn string
28 | )
29 |
30 | Convey("retrieveActor", t, func() {
31 | userRepositoryMock := &modelmock.UserProvider{}
32 | permissionRepositoryMock := &modelmock.PermissionProvider{}
33 | sessionMock := &mnemosynetest.SessionManagerClient{}
34 | h := &handler{
35 | session: sessionMock,
36 | }
37 | h.repository.user = userRepositoryMock
38 | h.repository.permission = permissionRepositoryMock
39 |
40 | Convey("As unauthenticated user", func() {
41 | ctx = context.Background()
42 | sessionMock.On("Context", mock.Anything, none(), mock.Anything).
43 | Return(nil, errors.New("mnemosyned: test error")).
44 | Once()
45 |
46 | Convey("Should return an error", func() {
47 | act, err = h.Actor(ctx)
48 |
49 | So(err, ShouldNotBeNil)
50 | So(act, ShouldBeNil)
51 | })
52 | })
53 | Convey("As authenticated user", func() {
54 | id = 7856282
55 | tkn = "0000000001hash"
56 | ctx = mnemosyne.NewAccessTokenContext(context.Background(), tkn)
57 | sessionMock.On("Context", ctx, none(), mock.Anything).
58 | Return(&mnemosynerpc.ContextResponse{
59 | Session: &mnemosynerpc.Session{
60 | AccessToken: tkn,
61 | SubjectId: session.ActorIDFromInt64(id).String(),
62 | },
63 | }, nil).
64 | Once()
65 |
66 | Convey("When user exists", func() {
67 | userRepositoryMock.On("FindOneByID", mock.Anything, id).
68 | Return(&model.UserEntity{ID: id}, nil).
69 | Once()
70 |
71 | Convey("And it has some Permissions", func() {
72 | permissionRepositoryMock.On("FindByUserID", mock.Anything, id).
73 | Return([]*model.PermissionEntity{
74 | {
75 | Subsystem: charon.PermissionCanRetrieve.Subsystem(),
76 | Module: charon.PermissionCanRetrieve.Module(),
77 | Action: charon.PermissionCanRetrieve.Action(),
78 | },
79 | {
80 | Subsystem: charon.UserCanRetrieveAsOwner.Subsystem(),
81 | Module: charon.UserCanRetrieveAsOwner.Module(),
82 | Action: charon.UserCanRetrieveAsOwner.Action(),
83 | },
84 | }, nil).
85 | Once()
86 |
87 | Convey("Then it should be retrieved without any error", func() {
88 | act, err = h.Actor(ctx)
89 |
90 | So(err, ShouldBeNil)
91 | So(act, ShouldNotBeNil)
92 | So(act.User.ID, ShouldEqual, id)
93 | So(act.Permissions, ShouldHaveLength, 2)
94 | })
95 | })
96 | Convey("And it has no Permissions", func() {
97 | permissionRepositoryMock.On("FindByUserID", mock.Anything, id).
98 | Return(nil, sql.ErrNoRows).
99 | Once()
100 |
101 | Convey("Then it should be retrieved without any error", func() {
102 | act, err = h.Actor(ctx)
103 |
104 | So(err, ShouldBeNil)
105 | So(act, ShouldNotBeNil)
106 | So(act.User.ID, ShouldEqual, id)
107 | So(act.Permissions, ShouldHaveLength, 0)
108 | })
109 | })
110 | })
111 | })
112 | })
113 | }
114 |
--------------------------------------------------------------------------------
/pb/rpc/charond/v1mock/permission_manager_client.go:
--------------------------------------------------------------------------------
1 | // Code generated by mockery v1.0.0. DO NOT EDIT.
2 |
3 | package charondmock
4 |
5 | import charond "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1"
6 | import context "context"
7 | import grpc "google.golang.org/grpc"
8 | import mock "github.com/stretchr/testify/mock"
9 |
10 | // PermissionManagerClient is an autogenerated mock type for the PermissionManagerClient type
11 | type PermissionManagerClient struct {
12 | mock.Mock
13 | }
14 |
15 | // Get provides a mock function with given fields: ctx, in, opts
16 | func (_m *PermissionManagerClient) Get(ctx context.Context, in *charond.GetPermissionRequest, opts ...grpc.CallOption) (*charond.GetPermissionResponse, error) {
17 | _va := make([]interface{}, len(opts))
18 | for _i := range opts {
19 | _va[_i] = opts[_i]
20 | }
21 | var _ca []interface{}
22 | _ca = append(_ca, ctx, in)
23 | _ca = append(_ca, _va...)
24 | ret := _m.Called(_ca...)
25 |
26 | var r0 *charond.GetPermissionResponse
27 | if rf, ok := ret.Get(0).(func(context.Context, *charond.GetPermissionRequest, ...grpc.CallOption) *charond.GetPermissionResponse); ok {
28 | r0 = rf(ctx, in, opts...)
29 | } else {
30 | if ret.Get(0) != nil {
31 | r0 = ret.Get(0).(*charond.GetPermissionResponse)
32 | }
33 | }
34 |
35 | var r1 error
36 | if rf, ok := ret.Get(1).(func(context.Context, *charond.GetPermissionRequest, ...grpc.CallOption) error); ok {
37 | r1 = rf(ctx, in, opts...)
38 | } else {
39 | r1 = ret.Error(1)
40 | }
41 |
42 | return r0, r1
43 | }
44 |
45 | // List provides a mock function with given fields: ctx, in, opts
46 | func (_m *PermissionManagerClient) List(ctx context.Context, in *charond.ListPermissionsRequest, opts ...grpc.CallOption) (*charond.ListPermissionsResponse, error) {
47 | _va := make([]interface{}, len(opts))
48 | for _i := range opts {
49 | _va[_i] = opts[_i]
50 | }
51 | var _ca []interface{}
52 | _ca = append(_ca, ctx, in)
53 | _ca = append(_ca, _va...)
54 | ret := _m.Called(_ca...)
55 |
56 | var r0 *charond.ListPermissionsResponse
57 | if rf, ok := ret.Get(0).(func(context.Context, *charond.ListPermissionsRequest, ...grpc.CallOption) *charond.ListPermissionsResponse); ok {
58 | r0 = rf(ctx, in, opts...)
59 | } else {
60 | if ret.Get(0) != nil {
61 | r0 = ret.Get(0).(*charond.ListPermissionsResponse)
62 | }
63 | }
64 |
65 | var r1 error
66 | if rf, ok := ret.Get(1).(func(context.Context, *charond.ListPermissionsRequest, ...grpc.CallOption) error); ok {
67 | r1 = rf(ctx, in, opts...)
68 | } else {
69 | r1 = ret.Error(1)
70 | }
71 |
72 | return r0, r1
73 | }
74 |
75 | // Register provides a mock function with given fields: ctx, in, opts
76 | func (_m *PermissionManagerClient) Register(ctx context.Context, in *charond.RegisterPermissionsRequest, opts ...grpc.CallOption) (*charond.RegisterPermissionsResponse, error) {
77 | _va := make([]interface{}, len(opts))
78 | for _i := range opts {
79 | _va[_i] = opts[_i]
80 | }
81 | var _ca []interface{}
82 | _ca = append(_ca, ctx, in)
83 | _ca = append(_ca, _va...)
84 | ret := _m.Called(_ca...)
85 |
86 | var r0 *charond.RegisterPermissionsResponse
87 | if rf, ok := ret.Get(0).(func(context.Context, *charond.RegisterPermissionsRequest, ...grpc.CallOption) *charond.RegisterPermissionsResponse); ok {
88 | r0 = rf(ctx, in, opts...)
89 | } else {
90 | if ret.Get(0) != nil {
91 | r0 = ret.Get(0).(*charond.RegisterPermissionsResponse)
92 | }
93 | }
94 |
95 | var r1 error
96 | if rf, ok := ret.Get(1).(func(context.Context, *charond.RegisterPermissionsRequest, ...grpc.CallOption) error); ok {
97 | r1 = rf(ctx, in, opts...)
98 | } else {
99 | r1 = ret.Error(1)
100 | }
101 |
102 | return r0, r1
103 | }
104 |
--------------------------------------------------------------------------------
/pb/rpc/charond/v1mock/refresh_token_manager_client.go:
--------------------------------------------------------------------------------
1 | // Code generated by mockery v1.0.0. DO NOT EDIT.
2 |
3 | package charondmock
4 |
5 | import charond "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1"
6 | import context "context"
7 | import grpc "google.golang.org/grpc"
8 | import mock "github.com/stretchr/testify/mock"
9 |
10 | // RefreshTokenManagerClient is an autogenerated mock type for the RefreshTokenManagerClient type
11 | type RefreshTokenManagerClient struct {
12 | mock.Mock
13 | }
14 |
15 | // Create provides a mock function with given fields: ctx, in, opts
16 | func (_m *RefreshTokenManagerClient) Create(ctx context.Context, in *charond.CreateRefreshTokenRequest, opts ...grpc.CallOption) (*charond.CreateRefreshTokenResponse, error) {
17 | _va := make([]interface{}, len(opts))
18 | for _i := range opts {
19 | _va[_i] = opts[_i]
20 | }
21 | var _ca []interface{}
22 | _ca = append(_ca, ctx, in)
23 | _ca = append(_ca, _va...)
24 | ret := _m.Called(_ca...)
25 |
26 | var r0 *charond.CreateRefreshTokenResponse
27 | if rf, ok := ret.Get(0).(func(context.Context, *charond.CreateRefreshTokenRequest, ...grpc.CallOption) *charond.CreateRefreshTokenResponse); ok {
28 | r0 = rf(ctx, in, opts...)
29 | } else {
30 | if ret.Get(0) != nil {
31 | r0 = ret.Get(0).(*charond.CreateRefreshTokenResponse)
32 | }
33 | }
34 |
35 | var r1 error
36 | if rf, ok := ret.Get(1).(func(context.Context, *charond.CreateRefreshTokenRequest, ...grpc.CallOption) error); ok {
37 | r1 = rf(ctx, in, opts...)
38 | } else {
39 | r1 = ret.Error(1)
40 | }
41 |
42 | return r0, r1
43 | }
44 |
45 | // List provides a mock function with given fields: ctx, in, opts
46 | func (_m *RefreshTokenManagerClient) List(ctx context.Context, in *charond.ListRefreshTokensRequest, opts ...grpc.CallOption) (*charond.ListRefreshTokensResponse, error) {
47 | _va := make([]interface{}, len(opts))
48 | for _i := range opts {
49 | _va[_i] = opts[_i]
50 | }
51 | var _ca []interface{}
52 | _ca = append(_ca, ctx, in)
53 | _ca = append(_ca, _va...)
54 | ret := _m.Called(_ca...)
55 |
56 | var r0 *charond.ListRefreshTokensResponse
57 | if rf, ok := ret.Get(0).(func(context.Context, *charond.ListRefreshTokensRequest, ...grpc.CallOption) *charond.ListRefreshTokensResponse); ok {
58 | r0 = rf(ctx, in, opts...)
59 | } else {
60 | if ret.Get(0) != nil {
61 | r0 = ret.Get(0).(*charond.ListRefreshTokensResponse)
62 | }
63 | }
64 |
65 | var r1 error
66 | if rf, ok := ret.Get(1).(func(context.Context, *charond.ListRefreshTokensRequest, ...grpc.CallOption) error); ok {
67 | r1 = rf(ctx, in, opts...)
68 | } else {
69 | r1 = ret.Error(1)
70 | }
71 |
72 | return r0, r1
73 | }
74 |
75 | // Revoke provides a mock function with given fields: ctx, in, opts
76 | func (_m *RefreshTokenManagerClient) Revoke(ctx context.Context, in *charond.RevokeRefreshTokenRequest, opts ...grpc.CallOption) (*charond.RevokeRefreshTokenResponse, error) {
77 | _va := make([]interface{}, len(opts))
78 | for _i := range opts {
79 | _va[_i] = opts[_i]
80 | }
81 | var _ca []interface{}
82 | _ca = append(_ca, ctx, in)
83 | _ca = append(_ca, _va...)
84 | ret := _m.Called(_ca...)
85 |
86 | var r0 *charond.RevokeRefreshTokenResponse
87 | if rf, ok := ret.Get(0).(func(context.Context, *charond.RevokeRefreshTokenRequest, ...grpc.CallOption) *charond.RevokeRefreshTokenResponse); ok {
88 | r0 = rf(ctx, in, opts...)
89 | } else {
90 | if ret.Get(0) != nil {
91 | r0 = ret.Get(0).(*charond.RevokeRefreshTokenResponse)
92 | }
93 | }
94 |
95 | var r1 error
96 | if rf, ok := ret.Get(1).(func(context.Context, *charond.RevokeRefreshTokenRequest, ...grpc.CallOption) error); ok {
97 | r1 = rf(ctx, in, opts...)
98 | } else {
99 | r1 = ret.Error(1)
100 | }
101 |
102 | return r0, r1
103 | }
104 |
--------------------------------------------------------------------------------
/pb/rpc/charond/v1/common.pb.go:
--------------------------------------------------------------------------------
1 | // Code generated by protoc-gen-go. DO NOT EDIT.
2 | // source: github.com/piotrkowalczuk/charon/pb/rpc/charond/v1/common.proto
3 |
4 | package charond // import "github.com/piotrkowalczuk/charon/pb/rpc/charond/v1"
5 |
6 | import proto "github.com/golang/protobuf/proto"
7 | import fmt "fmt"
8 | import math "math"
9 |
10 | // Reference imports to suppress errors if they are not otherwise used.
11 | var _ = proto.Marshal
12 | var _ = fmt.Errorf
13 | var _ = math.Inf
14 |
15 | // This is a compile-time assertion to ensure that this generated file
16 | // is compatible with the proto package it is being compiled against.
17 | // A compilation error at this line likely means your copy of the
18 | // proto package needs to be updated.
19 | const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
20 |
21 | // Order represents single field within OrderBy clause.
22 | type Order struct {
23 | Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
24 | Descending bool `protobuf:"varint,2,opt,name=descending,proto3" json:"descending,omitempty"`
25 | XXX_NoUnkeyedLiteral struct{} `json:"-"`
26 | XXX_unrecognized []byte `json:"-"`
27 | XXX_sizecache int32 `json:"-"`
28 | }
29 |
30 | func (m *Order) Reset() { *m = Order{} }
31 | func (m *Order) String() string { return proto.CompactTextString(m) }
32 | func (*Order) ProtoMessage() {}
33 | func (*Order) Descriptor() ([]byte, []int) {
34 | return fileDescriptor_common_6fa0804b816886fd, []int{0}
35 | }
36 | func (m *Order) XXX_Unmarshal(b []byte) error {
37 | return xxx_messageInfo_Order.Unmarshal(m, b)
38 | }
39 | func (m *Order) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
40 | return xxx_messageInfo_Order.Marshal(b, m, deterministic)
41 | }
42 | func (dst *Order) XXX_Merge(src proto.Message) {
43 | xxx_messageInfo_Order.Merge(dst, src)
44 | }
45 | func (m *Order) XXX_Size() int {
46 | return xxx_messageInfo_Order.Size(m)
47 | }
48 | func (m *Order) XXX_DiscardUnknown() {
49 | xxx_messageInfo_Order.DiscardUnknown(m)
50 | }
51 |
52 | var xxx_messageInfo_Order proto.InternalMessageInfo
53 |
54 | func (m *Order) GetName() string {
55 | if m != nil {
56 | return m.Name
57 | }
58 | return ""
59 | }
60 |
61 | func (m *Order) GetDescending() bool {
62 | if m != nil {
63 | return m.Descending
64 | }
65 | return false
66 | }
67 |
68 | func init() {
69 | proto.RegisterType((*Order)(nil), "charon.rpc.charond.v1.Order")
70 | }
71 |
72 | func init() {
73 | proto.RegisterFile("github.com/piotrkowalczuk/charon/pb/rpc/charond/v1/common.proto", fileDescriptor_common_6fa0804b816886fd)
74 | }
75 |
76 | var fileDescriptor_common_6fa0804b816886fd = []byte{
77 | // 174 bytes of a gzipped FileDescriptorProto
78 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xb2, 0x4f, 0xcf, 0x2c, 0xc9,
79 | 0x28, 0x4d, 0xd2, 0x4b, 0xce, 0xcf, 0xd5, 0x2f, 0xc8, 0xcc, 0x2f, 0x29, 0xca, 0xce, 0x2f, 0x4f,
80 | 0xcc, 0x49, 0xae, 0x2a, 0xcd, 0xd6, 0x4f, 0xce, 0x48, 0x2c, 0xca, 0xcf, 0xd3, 0x2f, 0x48, 0xd2,
81 | 0x2f, 0x2a, 0x48, 0x86, 0xf2, 0x52, 0xf4, 0xcb, 0x0c, 0xf5, 0x93, 0xf3, 0x73, 0x73, 0xf3, 0xf3,
82 | 0xf4, 0x0a, 0x8a, 0xf2, 0x4b, 0xf2, 0x85, 0x44, 0x21, 0x12, 0x7a, 0x45, 0x05, 0xc9, 0x7a, 0x50,
83 | 0x35, 0x7a, 0x65, 0x86, 0x4a, 0xd6, 0x5c, 0xac, 0xfe, 0x45, 0x29, 0xa9, 0x45, 0x42, 0x42, 0x5c,
84 | 0x2c, 0x79, 0x89, 0xb9, 0xa9, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, 0x9c, 0x41, 0x60, 0xb6, 0x90, 0x1c,
85 | 0x17, 0x57, 0x4a, 0x6a, 0x71, 0x72, 0x6a, 0x5e, 0x4a, 0x66, 0x5e, 0xba, 0x04, 0x93, 0x02, 0xa3,
86 | 0x06, 0x47, 0x10, 0x92, 0x88, 0x53, 0x02, 0x97, 0x42, 0x72, 0x7e, 0xae, 0x1e, 0xcc, 0x69, 0xd8,
87 | 0x2c, 0x08, 0x60, 0x8c, 0xb2, 0x22, 0xdd, 0xe9, 0xd6, 0x50, 0x66, 0x12, 0x1b, 0xd8, 0xf1, 0xc6,
88 | 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, 0xb2, 0x79, 0xac, 0x27, 0xff, 0x00, 0x00, 0x00,
89 | }
90 |
--------------------------------------------------------------------------------
/internal/model/permission_test.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "context"
5 | "reflect"
6 | "testing"
7 |
8 | "github.com/piotrkowalczuk/charon"
9 | )
10 |
11 | var (
12 | permissionTestFixtures = []*PermissionEntity{
13 | {
14 | Subsystem: "subsystem",
15 | Module: "module",
16 | Action: "action",
17 | },
18 | {
19 | Module: "module",
20 | Action: "action",
21 | },
22 | {
23 | Action: "action",
24 | },
25 | }
26 | )
27 |
28 | func TestPermissionRepository_FindOneByID(t *testing.T) {
29 | suite := &postgresSuite{}
30 | suite.setup(t)
31 | defer suite.teardown(t)
32 |
33 | for res := range loadPermissionFixtures(t, suite.repository.permission, permissionTestFixtures) {
34 | found, err := suite.repository.permission.FindOneByID(context.TODO(), res.got.ID)
35 |
36 | if err != nil {
37 | t.Errorf("permission cannot be found, unexpected error: %s", err.Error())
38 | continue
39 | }
40 |
41 | if assert(t, found != nil, "permission was not found, nil object returned") {
42 | assertf(t, reflect.DeepEqual(res.got, *found), "created and retrieved entity should be equal, but its not\ncreated: %#v\nfounded: %#v", res.got, found)
43 | }
44 | }
45 | }
46 |
47 | func TestPermissionRepository_Register(t *testing.T) {
48 | suite := &postgresSuite{}
49 | suite.setup(t)
50 | defer suite.teardown(t)
51 |
52 | data := []struct {
53 | created, removed, untouched int64
54 | permissions charon.Permissions
55 | }{
56 | {
57 | created: int64(len(charon.AllPermissions)),
58 | permissions: charon.AllPermissions,
59 | },
60 | {
61 | untouched: int64(len(charon.AllPermissions)),
62 | permissions: charon.AllPermissions,
63 | },
64 | {
65 | untouched: 1,
66 | removed: int64(len(charon.AllPermissions) - 1),
67 | permissions: charon.Permissions{
68 | charon.UserCanCreate,
69 | },
70 | },
71 | {
72 | created: 1,
73 | removed: 1,
74 | permissions: charon.Permissions{
75 | charon.Permission("charon:fakemodule:fakeaction"),
76 | },
77 | },
78 | {
79 | created: 1,
80 | permissions: charon.Permissions{
81 | charon.Permission("fakesystem:fakemodule:fakeaction"),
82 | },
83 | },
84 | {
85 | removed: 1,
86 | created: int64(len(charon.AllPermissions)),
87 | permissions: charon.AllPermissions,
88 | },
89 | }
90 |
91 | for i, d := range data {
92 | created, untouched, removed, err := suite.repository.permission.Register(context.TODO(), d.permissions)
93 | if err != nil {
94 | t.Fatalf("unexpected error: %s", err.Error())
95 | }
96 | if created != d.created {
97 | t.Errorf("expected different number of created permissions, expected %d got %d for set %d", d.created, created, i)
98 | }
99 | if untouched != d.untouched {
100 | t.Errorf("expected different number of untouched permissions, expected %d got %d for set %d", d.untouched, untouched, i)
101 | }
102 | if removed != d.removed {
103 | t.Errorf("expected different number of removed permissions, expected %d got %d for set %d", d.removed, removed, i)
104 | }
105 | }
106 | }
107 |
108 | type permissionFixtures struct {
109 | got, given PermissionEntity
110 | }
111 |
112 | func loadPermissionFixtures(t *testing.T, r PermissionProvider, f []*PermissionEntity) chan permissionFixtures {
113 | data := make(chan permissionFixtures, 1)
114 |
115 | go func() {
116 | for _, given := range f {
117 | entity, err := r.Insert(context.TODO(), given)
118 | if err != nil {
119 | t.Errorf("permission cannot be created, unexpected error: %s", err.Error())
120 | continue
121 | } else {
122 | t.Logf("permission has been created, got id %d", entity.ID)
123 | }
124 |
125 | data <- permissionFixtures{
126 | got: *entity,
127 | given: *given,
128 | }
129 | }
130 |
131 | close(data)
132 | }()
133 |
134 | return data
135 | }
136 |
--------------------------------------------------------------------------------
/internal/model/modelmock/user_groups_provider.go:
--------------------------------------------------------------------------------
1 | // Code generated by mockery v1.0.0. DO NOT EDIT.
2 |
3 | package modelmock
4 |
5 | import context "context"
6 | import mock "github.com/stretchr/testify/mock"
7 | import model "github.com/piotrkowalczuk/charon/internal/model"
8 |
9 | // UserGroupsProvider is an autogenerated mock type for the UserGroupsProvider type
10 | type UserGroupsProvider struct {
11 | mock.Mock
12 | }
13 |
14 | // DeleteByUserID provides a mock function with given fields: ctx, id
15 | func (_m *UserGroupsProvider) DeleteByUserID(ctx context.Context, id int64) (int64, error) {
16 | ret := _m.Called(ctx, id)
17 |
18 | var r0 int64
19 | if rf, ok := ret.Get(0).(func(context.Context, int64) int64); ok {
20 | r0 = rf(ctx, id)
21 | } else {
22 | r0 = ret.Get(0).(int64)
23 | }
24 |
25 | var r1 error
26 | if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
27 | r1 = rf(ctx, id)
28 | } else {
29 | r1 = ret.Error(1)
30 | }
31 |
32 | return r0, r1
33 | }
34 |
35 | // Exists provides a mock function with given fields: ctx, userID, groupID
36 | func (_m *UserGroupsProvider) Exists(ctx context.Context, userID int64, groupID int64) (bool, error) {
37 | ret := _m.Called(ctx, userID, groupID)
38 |
39 | var r0 bool
40 | if rf, ok := ret.Get(0).(func(context.Context, int64, int64) bool); ok {
41 | r0 = rf(ctx, userID, groupID)
42 | } else {
43 | r0 = ret.Get(0).(bool)
44 | }
45 |
46 | var r1 error
47 | if rf, ok := ret.Get(1).(func(context.Context, int64, int64) error); ok {
48 | r1 = rf(ctx, userID, groupID)
49 | } else {
50 | r1 = ret.Error(1)
51 | }
52 |
53 | return r0, r1
54 | }
55 |
56 | // Find provides a mock function with given fields: ctx, expr
57 | func (_m *UserGroupsProvider) Find(ctx context.Context, expr *model.UserGroupsFindExpr) ([]*model.UserGroupsEntity, error) {
58 | ret := _m.Called(ctx, expr)
59 |
60 | var r0 []*model.UserGroupsEntity
61 | if rf, ok := ret.Get(0).(func(context.Context, *model.UserGroupsFindExpr) []*model.UserGroupsEntity); ok {
62 | r0 = rf(ctx, expr)
63 | } else {
64 | if ret.Get(0) != nil {
65 | r0 = ret.Get(0).([]*model.UserGroupsEntity)
66 | }
67 | }
68 |
69 | var r1 error
70 | if rf, ok := ret.Get(1).(func(context.Context, *model.UserGroupsFindExpr) error); ok {
71 | r1 = rf(ctx, expr)
72 | } else {
73 | r1 = ret.Error(1)
74 | }
75 |
76 | return r0, r1
77 | }
78 |
79 | // Insert provides a mock function with given fields: ctx, ent
80 | func (_m *UserGroupsProvider) Insert(ctx context.Context, ent *model.UserGroupsEntity) (*model.UserGroupsEntity, error) {
81 | ret := _m.Called(ctx, ent)
82 |
83 | var r0 *model.UserGroupsEntity
84 | if rf, ok := ret.Get(0).(func(context.Context, *model.UserGroupsEntity) *model.UserGroupsEntity); ok {
85 | r0 = rf(ctx, ent)
86 | } else {
87 | if ret.Get(0) != nil {
88 | r0 = ret.Get(0).(*model.UserGroupsEntity)
89 | }
90 | }
91 |
92 | var r1 error
93 | if rf, ok := ret.Get(1).(func(context.Context, *model.UserGroupsEntity) error); ok {
94 | r1 = rf(ctx, ent)
95 | } else {
96 | r1 = ret.Error(1)
97 | }
98 |
99 | return r0, r1
100 | }
101 |
102 | // Set provides a mock function with given fields: ctx, userID, groupIDs
103 | func (_m *UserGroupsProvider) Set(ctx context.Context, userID int64, groupIDs []int64) (int64, int64, error) {
104 | ret := _m.Called(ctx, userID, groupIDs)
105 |
106 | var r0 int64
107 | if rf, ok := ret.Get(0).(func(context.Context, int64, []int64) int64); ok {
108 | r0 = rf(ctx, userID, groupIDs)
109 | } else {
110 | r0 = ret.Get(0).(int64)
111 | }
112 |
113 | var r1 int64
114 | if rf, ok := ret.Get(1).(func(context.Context, int64, []int64) int64); ok {
115 | r1 = rf(ctx, userID, groupIDs)
116 | } else {
117 | r1 = ret.Get(1).(int64)
118 | }
119 |
120 | var r2 error
121 | if rf, ok := ret.Get(2).(func(context.Context, int64, []int64) error); ok {
122 | r2 = rf(ctx, userID, groupIDs)
123 | } else {
124 | r2 = ret.Error(2)
125 | }
126 |
127 | return r0, r1, r2
128 | }
129 |
--------------------------------------------------------------------------------